실행환경

전자정부 표준프레임워크 실행환경은 ‘전자정부 서비스의 품질향상 및 정보화 투자 효율성 향상’을 위해 개발프레임워크 실행환경 표준을 정립하고, 개발프레임워크 표준 적용을 통한 응용 SW의 표준화 및 품질과 재사용성 향상을 목표로 한다. 또한, 모바일 웹의 사용성과 편의성 증대를 위하여 기존 실행환경 기반 개발이 가능한 모바일 웹 기반의 표준패턴 및 가이드 코드를 제공한다.

실행환경

전자정부 표준프레임워크 실행환경은 ‘전자정부 서비스의 품질향상 및 정보화 투자 효율성 향상’을 위해 개발프레임워크 실행환경 표준을 정립하고, 개발프레임워크 표준 적용을 통한 응용 SW의 표준화 및 품질과 재사용성 향상을 목표로 한다. 또한, 모바일 웹의 사용성과 편의성 증대를 위하여 기존 실행환경 기반 개발이 가능한 모바일 웹 기반의 표준패턴 및 가이드 코드를 제공한다.

1 - 공통기반 핵심

Spring IoC Container는 객체(빈) 관리, 의존성 주입, Bean의 초기화와 소멸 등을 제공하며, 다양한 스코프와 프로파일 설정을 지원한다. 또한, Spring은 XML 스키마 기반 AOP, AspectJ 어노테이션, 그리고 리소스를 활용한 메시지 제공 서비스 등을 통해 개발자의 생산성을 높인다.

공통기반 핵심

Spring IoC Container는 객체(빈) 관리, 의존성 주입, Bean의 초기화와 소멸 등을 제공하며, 다양한 스코프와 프로파일 설정을 지원한다. 또한, Spring은 XML 스키마 기반 AOP, AspectJ 어노테이션, 그리고 리소스를 활용한 메시지 제공 서비스 등을 통해 개발자의 생산성을 높인다.

1.1 - IoC Container

IoC 컨테이너는 객체 간의 종속성을 소스 코드 외부에서 설정하여 유연성과 확장성을 높이는 Spring 프레임워크의 핵심 기능이다.

IoC Container

개요

프레임워크의 기본적인 기능인 Inversion of Control(IoC) Container 기능을 제공하는 서비스이다.
객체의 생성 시, 객체가 참조하고 있는 타 객체에 대한 종속성을 소스 코드 내부에서 하드 코딩하는 것이 아닌, 소스 코드 외부에서 설정하게 함으로써, 유연성 및 확장성을 향상시킨다.

주요 개념

Inversion of Control(IoC)

IoC는 Inversion of Control의 약자이다. 우리나라 말로 직역해 보면 “역제어"라고 할 수 있다. 제어의 역전 현상이 무엇인지 살펴본다.
기존에 자바 기반으로 어플리케이션을 개발할 때 자바 객체를 생성하고 서로간의 의존 관계를 연결시키는 작업에 대한 제어권은 보통 개발되는 어플리케이션에 있었다.
그러나, Servlet, EJB 등을 사용하는 경우 Servlet Container, EJB Container에게 제어권이 넘어가서 객체의 생명주기(Life Cycle)를 Container들이 전담하게 된다.
이처럼 IoC에서 이야기하는 제어권의 역전이란 객체의 생성에서부터 생명주기의 관리까지 모든 객체에 대한 제어권이 바뀌었다는 것을 의미한다.

관련문서

Dependency Injection

각 클래스 사이의 의존관계를 빈 설정(Bean Definition)정보를 바탕으로 컨테이너가 자동적으로 연결해주는 것을 말한다.
컨테이너가 의존관계를 자동적으로 연결시켜주기 때문에 개발자들이 컨테이너 API를 이용하여 의존관계에 관여할 필요가 없게 되므로 컨테이너 API에 종속되는 것을 줄일 수 있다.
개발자들은 단지 빈 설정파일(저장소 관리 파일)에서 의존관계가 필요하다는 정보를 추가하기만 하면 된다.

관련문서

사용된 오픈 소스

설명

본 IoC Container는 Spring Framework의 기능을 수정없이 사용하는 것으로, 본 가이드 문서는 Spring Framework Documentation 을 번역 및 요약한 것이다.
Spring Framework IoC Container에 대한 상세한 설명이 필요한 경우, Spring Framework Documentation 원본 문서 및 Spring Framework API를 참조한다.

IoC Container of Spring Framework

org.springframework.beans과 org.springframework.context 패키지는 Spring Framework의 IoC Container의 기반을 제공한다.
BeanFactory 인터페이스는 객체를 관리하기 위한 보다 진보된 설정 메커니즘을 제공한다.
BeanFactory 인터페이스를 기반으로 작성된 ApplicationContext 인터페이스(BeanFactory 인터페이스의 sub-interface이다)는 BeanFactory가 제공하는 기능 외에 Spring AOP, 메시지 리소스 처리(국제화에서 사용됨), 이벤트 전파, 웹 어플리케이션을 위한 WebSpplicationContext 등 어플리케이션 레이어에 특화된 context 등의 기능을 제공한다.

요약하면, BeanFactory는 프레임워크와 기본적인 기능에 대한 설정 기능을 제공하는 반면에, ApplicationContext는 좀더 Enterprise 환경에 맞는 기능들을 추가로 제공한다.
ApplicationContext는 BeanFacatory의 완전한 superset이므로, BeanFactory의 기능 및 행동에 대한 설명은 ApplicationContext에도 모두 해당된다.

본 문서는 크게 두 부분으로 나뉘어지는데, 첫번째 부분은 BeanFactory와 ApplicationContext 모두에 적용되는 기본적인 원리를 설명하고, 두번째 부분은 ApplicationContext에만 적용되는 특징들을 설명한다.

참고자료

1.2 - IoC Container Basics

Spring Framework에서 제어의 역전(Inversion of Control, IoC)은 객체가 생성자 인수, 팩토리 메서드 인수, 또는 객체 인스턴스에 설정된 속성을 통해서만 다른 객체에 대한 종속성을 정의하는 프로세스를 말한다. 의존성 주입(Dependency Injection, DI)은 IoC의 한 종류로, 모듈 간의 의존성을 외부 컨테이너에서 주입해 주는 기능이다.

Basics

개요

Spring Framework에서 객체가 생성자 인수, 팩토리 메서드에 대한 인수 또는 객체 인스턴스가 생성되거나 팩토리 메서드에서 반환된 후 객체 인스턴스에 설정된 속성을 통해서만 종속성(함께 작업하는 다른 객체)을 정의하는 프로세스를 제어의 역전(Inversion of Control, IoC)라고 한다. 의존성 주입(Dependency Injection, DI)은 모듈간의 의존성을 모듈의 외부 컨테이너 에서 주입시켜주는 기능으로 IoC의 한 종류이다.

설명

Spring Framework에서 Bean은 어플리케이션을 구성하고, IoC Container에 의해 관리되어지는 객체로 간단히 말해 IoC Container에 의해 객체화되고, 조립되고, 또는 관리되는 객체를 의미한다.
Bean들과 Bean들간의 종속성은 Container가 사용하는 설정 메타데이터에 의해 결정된다.

The Container

org.springframework.beans.factory.BeanFactory 인터페이스는 Spring IoC Container의 핵심 인터페이스로 Spring IoC Container는 객체를 생성하고, 객체간의 종속성을 이어주는 역할을 한다.

 Spring IoC Container

설정 정보(Configuration Metadata)

위 그림에서 보듯이, Spring IoC Container는 설정 정보(configuration metadata)를 필요로 한다. 이 설정 정보는 Spring IoC Container가 “객체를 생성하고, 객체간의 종속성을 이어줄 수 있도록” 필요한 정보를 제공한다.
설정 정보는 일반적으로 XML 형태로 작성된다. 설정 정보는 XML 형태가 아닌 Java Annotation을 이용하여 설정이 가능하다.
Annotation을 사용한 설정 방법은 Annotation-based configuration에서 설명하고 있다.

아래 예제는 XML 형태의 설정 정보의 기본적인 모습이다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
 
   <bean id="..." class="...">
       <!-- collaborators and configuration for this bean go here -->
   </bean>
 
   <bean id="..." class="...">
       <!-- collaborators and configuration for this bean go here -->
   </bean>
 
   <!-- more bean definitions go here -->
</beans>

<beans> tag는 Spring IoC Container의 설정 정보를 나타내는 tag이다. 그리고 각각의 <bean> tag는 Spring IoC Container가 생성하고, 관리할 객체의 정의를 나타낸다.
XML 설정 정보를 여러 개의 파일로 나뉘어 구성될 수 있다. 이 경우, 전체 설정 정보를 읽기 위해서 하나의 설정 파일에서 다른 파일을 import할 수 있다. Import 하는 방법으로 <import> tag를 사용한다.

<beans>
   <import resource="services.xml"/>
   <import resource="resources/messageSource.xml"/>
   <import resource="/resources/themeSource.xml"/>
 
   <bean id="bean1" class="..."/>
   <bean id="bean2" class="..."/>
</beans>

<import> tag의 resource attribute는 import할 XML 설정 파일의 위치를 나타낸다.

Container 객체화

다음은 Container를 객체화하는 한 예이다.

ApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"});

// an Application is also a BeanFactory (via inheritance)
BeanFactory factory = context;

위 예제의 ClassPathXmlApplicationContext는 ApplicationContext의 한 종류이며, ApplicationContext 인터페이스는 BeanFactory 인터페이스를 상속하고 있다.

Container 사용 방법

Container를 객체화하면 getBean(String) 메소드를 사용하여 bean을 가져올 수 있다.

The beans

Spring IoC Container는 다수의 bean들을 관리한다. Container는 설정 정보를 사용하여 bean들은 생성한다. Container에서 사용하는 bean 정의는 아래 정보를 담고 있다.

  • 클래스 이름(a package-qualified class name): bean의 실제 구현 클래스를 나타낸다.

  • Bean 행동 정보(bean behavioral configuration elements): Container 안에서 bean이 어떤 식으로 행동하는지에 대한 정보를 나타낸다.(scope, lifecycle callbacks 등등)

  • 다른 bean에 대한 참조(references to other beans): bean이 동작하기 위해 필요한 다른 bean들에 대한 참조 정보를 나타낸다. 이런 참조는 협력자(collaborators) 또는 종속성(dependencies)라고도 한다.

  • 기타 객체에 설정할 정보들(other configuration settings): connection pool을 관리하는 bean에서 사용할 connection의 개수, 또는 pool의 최대 크기 등

위 개념적인 정보들은 실제 <bean> tag로 작성된다. <bean> tag를 구성하는 bean 정의는 아래 표와 같다.

FeatureExplained in…
classBean 객체화(Instantiation beans)
nameBean 이름(Naming beans)
scopeBean scope
constructor arguments종속성 삽입(Injecting dependencies)
properties종속성 삽입(Injecting dependencies)
autowiring mode자동엮기(Autowiring collaborators)
dependency checking mode종속성 검사(Checking for dependencies)
lazy-initialization mode늦은 객체화(Lazily-instantiated beans)
initialization method객체화 callbacks(Initialization callbacks)
destruction method파괴 callbacks(Destruction callbacks)

Bean 이름(Naming beans)

모든 bean은 하나 이상의 id를 가져야 하며, 각각의 id는 Container안에서 단 하나만 존재해야 한다. 일반적으로 대부분의 bean은 하나의 id를 가지지만, 별명(alias)를 사용하여 둘 이상의 id를 가질 수도 있다.
Bean id에 대한 명명 규칙은 Java의 class field 명명 규칙과 같다. id는 소문자로 시작하고, 두번째 단어부터는 첫글자는 대문자로 작성한다. ‘accountManager’, ‘accountService’, ‘userDao’, ’loginController’ 등

Bean 별명(Aliasing beans)

<alias> tag를 사용하여 이미 정의된 bean에게 추가적인 이름을 부여할 수 있다.

<alias name="fromName" alias="toName"/>

name attribute는 대상이 되는 bean의 이름이고, alias attribute는 부여할 새로운 이름이다.

Bean 객체화(Instantiation beans)

모든 bean 정의는 객체화를 위해 실제 Java Class가 필요하다.
XML 설정에서는 ‘class’ attribute를 통해 Java Class를 설정한다. 대부분의 경우 Container는 bean를 객체화하기 위해서 Java의 ’new’ 연산자를 사용한다.
또는 특수한 경우, static 메소드를 사용할 수도 있다. 본 문서는 생성자를 이용한 객체화만을 설명한다.
생성자를 이용한 객체화는 가장 일반적인 방식으로, 다음과 같이 사용한다.

<bean id="exampleBean" class="example.ExampleBean"/>
 
<bean name="anotherExample" class="examples.ExampleBeanTwo"/>

참고자료

1.3 - Dependencies

일반적인 엔터프라이즈 애플리케이션은 여러 객체가 협력하여 작동하며, Spring에서는 이러한 객체들을 각각 독립적인 빈으로 정의한다. Spring 프레임워크를 통해 독립적으로 정의된 빈들이 협업하여 애플리케이션의 목표를 달성하는 방법을 설명한다.

Dependencies

개요

일반적인 엔터프라이즈 애플리케이션은 단일 객체(또는 Spring 용어로 빈)로만 이루어지지 않고 간단한 애플리케이션도 최종 사용자에게 일관된 사용자 경험을 제공하기 위해 여러 객체가 함께 작동한다. 이러한 객체들은 독립적으로 존재하며, Spring 프레임워크를 사용하여 각각의 빈으로 정의된다. 여기서는 독립적으로 정의된 여러 빈들이 협업하여 목표를 달성하는 방법에 대해 설명한다.

설명

종속성 삽입(Injecting dependencies)

종속성 삽입(Dependency Injection(DI))의 기본적인 원칙은 객체는 단지 생성자나 set 메소드를 통해서만 종속성(필요로 하는 객체)를 정의한다는 것이다.
그러면 Container는 Bean 객체를 생성할 때, Bean이 정의한 종속성을 추가하게 되는데 이는 Bean이 스스로 필요한 객체를 생성하거나 찾는 등의 제어를 가지는 것과는 반대의 개념으로 Inversion of Control(IoC)라고 부른다.
종속성 삽입에는 두 가지 방법이 있다. Constructor InjectionSetter Injection이다.

Constructor Injection

생성자(Constructor) 기반의 DI는 다수의 arguments를 갖는 생성자를 호출하여 종속성을 주입한다. <constructor-arg> element를 사용한다.

package x.y;
 
public class Foo {
   public Foo(Bar bar, Baz baz) {
       // ...
   }
}
<beans>
   <bean name="foo" class="x.y.Foo">
       <constructor-arg>
           <bean class="x.y.Bar"/>
       </constructor-arg>
       <constructor-arg>
           <bean class="x.y.Baz"/>
       </constructor-arg>
   </bean>
</beans>

만약, <value>true</value>와 같이 type이 명확하지 않은 값을 사용하는 경우, Spring은 생성자의 어떤 argument에 해당하는지 결정할 수 없다.

package examples;

public class ExampleBean {
   // No. of years to the calculate the Ultimate Answer
   private int years;

   // The Answer to Life, the Universe, and Everything
   private String ultimateAnswer;

   public ExampleBean(int years, String ultimateAnswer) {
       this.years = years;
       this.ultimateAnswer = ultimateAnswer;
   }
}
Constructor Argument Type Matching

위와 같은 경우, ’type’ attribute를 통해서 각 argument의 타입을 지정할 수 있다.

<bean id="exampleBean" class="examples.ExampleBean">
   <constructor-arg type="int" value="7500000"/>
   <constructor-arg type="java.lang.String" value="42"/>
</bean>
Constructor Argument Index

위와 같은 경우 ‘index’ attribute를 통해서 각 argument의 위치를 지정할 수 있다.

<bean id="exampleBean" class="examples.ExampleBean">
   <constructor-arg index="0" value="7500000"/>
   <constructor-arg index="1" value="42"/>
</bean>

(* index는 0부터 시작한다.)

Setter Injection

Setter 기반의 DI는 argument가 없는 생성자를 통해 bean 객체가 생성된 후, setter 메소드를 호출하여 종속성을 주입한다. <property> element를 사용한다.

<bean id="exampleBean" class="examples.ExampleBean">
   <!-- setter injection using the nested <ref/> element -->
   <property name="beanOne"><ref bean="anotherExampleBean"/></property>
 
   <!-- setter injection using the neater 'ref' attribute -->
   <property name="beanTwo" ref="yetAnotherBean"/>
   <property name="integerProperty" value="1"/>
</bean>
 
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
public class ExampleBean {
   private AnotherBean beanOne;
   private YetAnotherBean beanTwo;
   private int i;
 
   public void setBeanOne(AnotherBean beanOne) {
       this.beanOne = beanOne;
   }
 
   public void setBeanTwo(YetAnotherBean beanTwo) {
       this.beanTwo = beanTwo;
   }
 
   public void setIntegerProperty(int i) {
       this.i = i;
   }    
}

종속성 상세 설정(Dependencies and configuration in detail)

본 장은 종속성 삽입에 사용되는 <constructor-arg>와 <property> element의 sub-element type을 설명한다.

명확한 값(Straight values(primitives, Strings, etc.))

사람이 인식 가능한 문자열 형태를 <value> tag를 사용하여 표현한다. String을 argument나 property의 type에 맞춰 변환해준다.

<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
   <!-- results in a setDriverClassName(String) call -->
   <property name="driverClassName">
       <value>com.mysql.jdbc.Driver</value>
   </property>
   <property name="url">
       <value>jdbc:mysql://localhost:3306/mydb</value>
   </property>
   <property name="username">
       <value>root</value>
   </property>
   <property name="password">
       <value>masterkaoli</value>
   </property>
</bean>

<value> element 대신 ‘value’ attribute를 사용할 수도 있다.

<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
   <!-- results in a setDriverClassName(String) call -->
   <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
   <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
   <property name="username" value="root"/>
   <property name="password" value="masterkaoli"/>
</bean>

다른 bean 참조(References to other beans(collaborators))

ref element는 Container 안에 있는 다른 bean을 참조한다. 참조할 객체를 지정하는 방식에는 3가지가 있다.

  1. bean attribute
    가장 일반적인 형태로 같은 Container 또는 부모 Container에 포함된 bean 객체를 참조한다. ‘bean’ attribute는 대상 bean의 ‘id’ 또는 여러 ’name’들 중. 하나와 같아야 한다.

    <ref bean="someBean"/>
    
  2. local attribute
    같은 XML 설정 파일 내의 bean 객체를 참조한다. ’local’ attribute는 반드시 대상 bean의 ‘id’와 같아야 한다. 만약 대상 bean이 같은 XML 파일에 존재한다면. local을 사용하는 것이 좋다.

    <ref local="someBean"/>
    
  3. parent attribute
    현재 Container의 부모 Container의 bean 객체를 참조한다. ‘parent’ attribute는 대상 bean의 ‘id’ 또는 여러 ’name’들 중 하나와 같아야 한다.

    <!-- in the parent context -->
    <bean id="accountService" class="com.foo.SimpleAccountService">
       <!-- insert dependencies as required as here -->
    </bean>
    
    <!-- in the child (descendant) context -->
    <bean id="accountService"  <-- notice that the name of this bean is the same as the name of the 'parent' bean
       class="org.springframework.aop.framework.ProxyFactoryBean">
       <property name="target">
           <ref parent="accountService"/>  <-- notice how we refer to the parent bean
       </property>
       <!-- insert other configuration and dependencies as required as here -->
    </bean>
    

Inner beans

<property/> 또는 <constructor-arg/> element 안에 있는 <bean/> element를 inner bean이라고 한다. Inner bean은 id나 name을 정의할 필요가 없다. 정의한다 해도 Container에서 무시하기 때문에 정의하지 않는 것이 좋다.

<bean id="outer" class="...">
   <!-- instead of using a reference to a target bean, simply define the target bean inline -->
   <property name="target">
       <bean class="com.example.Person"> <!-- this is the inner bean -->
           <property name="name" value="Fiona Apple"/>
           <property name="age" value="25"/>
       </bean>
   </property>
</bean>

Inner bean의 ‘scope’ flag와 ‘id’, ’name’은 무시된다. Inner bean의 scope은 항상 prototype이다. 따라서 inner bean을 다른 bean에 주입하는 것은 불가능한다.

Collections

Java Collection 타입인 List, Set, Map, Properties를 표현하기 위해 <list/>, <set/>, <map/>, <props/> element가 사용된다.

<bean id="moreComplexObject" class="example.ComplexObject">
   <!-- results in a setAdminEmails(java.util.Properties) call -->
   <property name="adminEmails">
       <props>
           <prop key="administrator">administrator@example.org</prop>
           <prop key="support">support@example.org</prop>
           <prop key="development">development@example.org</prop>
       </props>
   </property>
   <!-- results in a setSomeList(java.util.List) call -->
   <property name="someList">
   <list>
       <value>a list element followed by a reference</value>
       <ref bean="myDataSource" />
   </list>
   </property>
   <!-- results in a setSomeMap(java.util.Map) call -->
   <property name="someMap">
       <map>
           <entry>
               <key>
                   <value>an entry</value>
               </key>
               <value>just some string</value>
           </entry>
           <entry>
               <key>
                   <value>a ref</value>
               </key>
               <ref bean="myDataSource" />
           </entry>
       </map>
   </property>
   <!-- results in a setSomeSet(java.util.Set) call -->
   <property name="someSet">
       <set>
           <value>just some string</value>
           <ref bean="myDataSource" />
       </set>
   </property>
</bean>

map의 key와 value, set의 value의 값은 아래 element 중 하나가 될 수 있다.

bean | ref | idref | list | set | map | props | value | null
Collection 병합(Collection merging)

Container는 collection 병합 기능을 제공한다. Bean 정의 상속을 사용하여 부모 bean 정의의 <list/>, <map/>, <set/>, <props/> element와 자식 bean 정의의 <list/>, <map/>, <set/>, <props/> element를 병합할 수 있다.

<beans>
<bean id="parent" abstract="true" class="example.ComplexObject">
   <property name="adminEmails">
       <props>
           <prop key="administrator">administrator@example.com</prop>
           <prop key="support">support@example.com</prop>
       </props>
   </property>
</bean>
<bean id="child" parent="parent">
   <property name="adminEmails">
       <!-- the merge is specified on the *child* collection definition -->
       <props merge="true">
           <prop key="sales">sales@example.com</prop>
           <prop key="support">support@example.co.uk</prop>
       </props>
   </property>
</bean>
<beans>

위 설정에 따라 생성된 child bean 객체의 adminEmails는 아래와 같은 값을 가진다.

administrator=administrator@example.com
sales=sales@example.com
support=support@example.co.uk

Nulls

null 값을 사용하기 위해서 <null/> element를 사용한다. Spring는 argument가 없을 경우 빈 문자열(””)로 인식한다.

<bean class="ExampleBean">
   <property name="email"><value/></property>
</bean>

위 설정에 따르면, email의 값은 ”“이다. 다음은 null값을 갖는 예제이다.

<bean class="ExampleBean">
   <property name="email"><null/></property>
</bean>

간편한 설정 방법(Shortcuts and other convenience options for XML-based configuration metadata)

XML-based configuration metadata shortcuts

<property/>, <constructor-arg/>, <entry/> element는 모두 <value/> element 대신에 ‘value’ attribute를 사용할 수 있다.

<property name="myProperty">
   <value>hello</value>
</property>
<constructor-arg>
   <value>hello</value>
</constructor-arg>
<entry key="myKey">
   <value>hello</value>
</entry>

위 설정은 아래와 동일한 설정이다.

<property name="myProperty" value="hello"/>
<constructor-arg value="hello"/>
<entry key="myKey" value="hello"/>

<property/>, <constructor-arg/> element는 <ref/> element 대신에 ‘ref’ attribute를 사용할 수 있다.

<property name="myProperty">
   <ref bean="myBean">
</property>
<constructor-arg>
   <ref bean="myBean">
</constructor-arg>

위 설정은 아래와 동일한 설정이다.

<property name="myProperty" ref="myBean"/>
<constructor-arg ref="myBean"/>

단, shortcut은 <ref bean=“xxx”>와 동일하다. <ref local=“xxx”>에 해당하는 shortcut은 없다.

<entry/> element는 ‘key’ / ‘key-ref’와 ‘value’ / ‘value-ref’ attribute를 사용할 수 있다.

<entry>
   <key>
       <ref bean="myKeyBean" />
   </key>
   <ref bean="myValueBean" />
</entry>

위 설정은 아래와 같은 설정이다.

<entry key-ref="myKeyBean" value-ref="myValueBean"/>
The p-namespace and how to use it to configure properties

‘’<property/>’’ element 대신 “p-namespace”를 사용하여 XML 설정을 작성할 수 있다. 아래 classic bean과 p-namespace bean은 동일한 Bean 설정이다.

<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:p="http://www.springframework.org/schema/p"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
 
   <bean name="classic" class="com.example.ExampleBean">
       <property name="email" value="foo@bar.com/>
   </bean>
 
   <bean name="p-namespace"
       class="com.example.ExampleBean"
       p:email="foo@bar.com"/>
</beans>

아래 예제는 다른 bean 객체의 참조를 삽입하는 예제이다. Attribute 이름 끝에 ‘-ref’를 붙이면 참조로 인식한다.

<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:p="http://www.springframework.org/schema/p"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
 
   <bean name="john-classic" class="com.example.Person">
       <property name="name" value="John Doe"/>
       <property name="spouse" ref="jane"/>
   </bean>
 
   <bean name="john-modern" 
       class="com.example.Person"
       p:name="John Doe"
       p:spouse-ref="jane"/>
 
   <bean name="jane" class="com.example.Person">
       <property name="name" value="Jane Doe"/>
   </bean>
</beans>

Compound property names

복합 형식의 property 이름도 사용 가능하다.

<bean id="foo" class="foo.Bar">
   <property name="fred.bob.sammy" value="123" />
</bean>

foo bean은 fred property를 가지고, fred property는 bob property를 가진다. 그리고 bob property는 sammy property를 가지고, 마지막 sammy property가 123을 값으로 가진다. 이 작업이 정상적으로 동작하려면 bean이 생성되었을 때, foo의 fred property, fred의 bob property는 반드시 null이 아니어야 한다. 그렇지 않을 경우 NullPointerException이 발생한다.

depends-on 사용(Using depends-on)

대부분의 경우, bean들간의 종속성은 ‘’<ref/>’’ element에 의해 표현된다. 하지만 드물게 이런 종속성이 직접 나타나지 않는 경우도 있다(예를 들면, database driver 등록처럼 static 메소드에 의해 초기화되어야 하는 경우 등). 이런 경우 ‘depends-on’ attribute를 사용하여 명시적으로 종속성을 표현할 수 있다.

<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
 
<bean id="manager" class="ManagerBean" />

만약 다수의 bean에 대한 종속성을 표현하고 하는 경우에는, ‘depends-on’ attribute의 값으로 bean 이름을 나열하면 된다. bean 이름의 구분자로는 콤마(’,’), 공백문자(’ ‘), 세미콜론(’;’) 등을 사용할 수 있다.

<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
   <property name="manager" ref="manager" />
</bean>
 
<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />

늦은 객체화(Lazily-instantiated beans)

ApplicationContext는 시작시에 모든 singleton bean을 선객체화(pre-instantiate)한다. 선객체화(pre-instantiate)는 초기화 과정에서 모든 singleton baen을 생성하고 설정한다는 것을 의미한다. 일반적으로 선객체화가 좋은 방식인데, 왜냐하면 잘못된 설정이 있는 경우, 즉시 발견할 수 있기 때문이다.

어쨌거나, 이런 방식을 원하지 않을 경우도 있다. 만약 ApplicationContext에 의해 선 객체화 되는 singleton bean을 원하지 않을 경우, 선택적으로 bean 정의에 늦은 객체화(lazy-initailized)를 설정할 수 있다. 늦은 객체화(lazy-initailized)로 설정된 bean은 시작 시에 생성되는 것이 아니라, 처음으로 필요로 했을 때 생성된다.

XML 설정에서는 ‘’<bean/>’’ element의 ’lazy-init’ attribute를 사용한다.

<bean id="lazy" class="com.foo.ExpensiveToCreateBean" lazy-init="true"/>
 
<bean name="not.lazy" class="com.foo.AnotherBean"/>

늦은 객체화에 대해서 이해하고 있어야 하는 것은, 만약 늦은 객체화로 설정된 bean에 대해서 그렇지 않은 singleton bean이 종속성을 가지고 있다면, ApplicationContext는 시작 시에 singleton bean이 종속하고 있는 모든 bean을 생성한다는 것이다. 즉, 명시적으로 늦은 객체화로 선언한 bean이라도 시작 시에 생성될 수 있다.

그리고, <beans/> element의 ‘default-lazy-init’ attribute를 사용하여 Container 레벨에서의 늦은 객체화를 설정할 수 있다.

<beans default-lazy-init="true">
   <!-- no beans will be pre-instantiated... -->
</beans>

자동엮기(Autowiring collaborators)

Spring Container는 서로 관계된 bean들을 자동으로 엮어(autowire)줄 수 있다. 자동엮기(autowiring)는 각각의 bean 단위로 설정된다. 자동엮기(autowiring) 기능을 사용하면 property나 생성자 argument를 지정할 필요가 없어지므로, 타이핑일 줄일 수 있다. 자동엮기(autowiring)에는 5가지 모드가 있으며, XML 기반 설정에서는 <bean/> element의 ‘autowire’ attribute를 사용하여 설정할 수 있다.

Mode설명
no자동엮기를 사용하지 않는다. Bean에 대한 참조는 ref element를 사용하여 지정해야만 한다. 이 모드가 기본(default)이다.
byNameProperty 이름으로 자동엮기를 수행한다. Property의 이름과 같은 이름을 가진 bean을 찾아서 엮어준다.
byTypeProperty 타입으로 자동엮기를 수행한다. Property의 타입과 같은 타입을 가진 bean을 찾아서 엮어준다. 만약 같은 타입을 가진 bean이 Container에 둘 이상 존재할 경우 exception이 발생한다. 만약 같은 타입을 가진 bean이 존재하지 않는 경우, 아무 일도 발생하지 않는다; 즉, property에는 설정되지 않는다.
constructorbyType과 유사하지만, 생성자 argument에만 적용된다. 만약 같은 타입의 bean이 존재하지 않거나 둘 이상 존재할 경우, exception이 발생한다.
autodetectBean class의 성질에 따라 constructorbyType 모드 중 하나를 선택한다. 만약 default 생성자가 존재하면, byType 모드가 적용된다.

만약 종속성을 property나 constructor-arg를 사용하여 명시적으로 설정한 경우, 자동엮기(autowiring) 설정은 무시된다.

Bean을 자동엮기 대상에서 제외하는 방법(Excluding a bean from being available from autowiring

<bean/> element의 ‘autowire-candidate’ attribute 값을 ‘false’로 설정함으로써, 대상 bean이 다른 bean에 의해 자동엮임을 당하는 것을 방지할 수 있다.

종속성 검사(Checking for dependencies)

Spring IoC Container는 bean의 미해결 종속성의 존재를 검사할 수 있다. 이 기능은 bean의 모든 property가 지정되었는지는 확인하고 싶을 때 유용하다. 종속성 검자(Dependency checking) 기능은 자동엮기(autowiring) 기능과 마찬가지로 각각의 bean마다 설정할 수 있다. 종속성 검사에는 4가지 모드가 있으며, XML 기반 설정에서는 <bean/> element의 ‘dependency-check’ attribute를 사용하여 설정할 수 있다.

Mode설명
none종속성 검사를 하지 않는다. 기본(default) 모드이다.
simplePrimitive 타입과 collection에 대해서 종속성 검사를 수행한다.
object관련된 객체에 대해서만 종속성 검사를 수행한다.
allPrimitive 타입과 collection, 관련된 객체에 대해서 종속성 검사를 수행한다.

메소드 삽입(Method Injection)

대부분의 어플리케이션에서, Container에 존재하는 대부분의 bean은 singleton이다. Singleton bean이 다른 singleton bean과 협력(collaborate)하거나, non-singleton bean이 다른 non-singleton bean과 협력하는 경우, 가장 일반적인 방법은 bean의 property를 정의함으로써 종속성을 조절하는 것이다. 하지만 만약 관련된 bean들의 생명주기가 다른 경우 문제가 발생한다. Singleton bean A가 non-singleton bean B를 사용한다고 할 때, Container는 singleton bean A를 단지 한번만 생성할 것이고, 따라서 property도 역시 한번만 설정될 것이다. Container는 bean B가 필요한 매 순간 새로운 객체를 생성하여 bean A에게 제공해야 하지만, 그럴 수 있는 방법이 없다.

위 문제에 대한 한가지 해법은 몇몇 제어의 역전(inversion of control)를 버리는 것이다. Bean A는 BeanFactoryAware interface를 구현함으로써 자신이 속한 Container를 알 수 있다. 그리고 bean B의 객체가 필요한 순간에 Container의 getBean(“B”)을 호출함으로써 bean B의 객체를 가져올 수 있다.

// a class that uses a stateful Command-style class to perform some processing
package fiona.apple;

// lots of Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;

public class CommandManager implements BeanFactoryAware {
   private BeanFactory beanFactory;

   public Object process(Map commandState) {
       // grab a new instance of the appropriate Command
       Command command = createCommand();
       // set the state on the (hopefully brand new) Command instance
       command.setState(commandState);
       return command.execute();
   }

   // the Command returned here could be an implementation that executes asynchronously, or whatever
   protected Command createCommand() {
       return (Command) this.beanFactory.getBean("command"); // notice the Spring API dependency
   }

   public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
       this.beanFactory = beanFactory;
   }
}

위 예제는 일반적으로는 바람직하지 않은 솔루션이다. 왜냐하면 업무 코드(business code)는 Spring Framework과 관련될 필요가 없기 때문이다. 메소드 삽입(Method Injection)은 이런 경우를 말끔히 해결할 수 있는 방법이다.

Lookup 메소드 삽입(Lookup method injection)

Lookup 메소드 삽입은 Container가 관리하고 있는 bean의 메소드를 덮어써서(override) Container 안에 있는 다른 bean을 찾을 수 있게 하는 기능이다. Spring Framework는 메소드 삽입을 구현하기 위해서 CGLIB 라이브러리를 사용하여 동적으로 상속클래스를 생성한다.

package fiona.apple;

// no more Spring imports! 
public abstract class CommandManager {
   public Object process(Object commandState) {
       // grab a new instance of the appropriate Command interface
       Command command = createCommand();
       // set the state on the (hopefully brand new) Command instance
       command.setState(commandState);
       return command.execute();
   }

   // okay... but where is the implementation of this method?
   protected abstract Command createCommand();
}

삽입될 메소드는 반드시 다음과 같은 형태를 가져야 한다.

<public|protected> [abstract] <return-type> theMethodName(no-arguments);

만약 메소드가 abstract이면, 동적으로 생성된 서브클래스는 메소드를 구현할 것이다. 만약 그렇지 않으면 동적으로 생성된 서브클래스를 원본 클래스의 메소드를 덮어쓸(override) 것이다.

<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="command" class="fiona.apple.AsyncCommand" scope="prototype">
   <!-- inject dependencies here as required -->
</bean>
 
<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
   <lookup-method name="createCommand" bean="command"/>
</bean>

commandManagercommand bean의 새로운 객체가 필요할 때마다 자신의 createCommand() 메소드를 호출할 것이다. 만약 command bean이 prototype이 아닌 singleton인 경우, createCommand 메소드는 같은 객체를 리턴할 것이다.

동적 서브클래스 생성이 동작하려면 classpath에 CGLIB가 추가되어 있어야 한다. 그리고 원본 class는 final이면 안되며, 덮어쓸(override) 메소드 역시 final이면 안된다.

참고자료

1.4 - Bean Scopes

Bean 정의는 실제 Bean 객체를 생성하는 방식을 규정하며, 하나의 Bean 정의에서 여러 객체를 생성할 수 있다. 이를 통해 객체에 다양한 종속성 및 설정값을 주입할 수 있으며, 객체의 범위(Scope)도 정의할 수 있다.

Bean scope

개요

Bean 정의는 실제 Bean 객체를 생성하는 방식을 정의하는 것으로 Class와 마찬가지로 하나의 Bean 정의에 해당하는 다수의 객체가 생성될 수 있다.
Bean 정의를 통해 객체에 다양한 종속성 및 설정값을 주입할 수 있을 뿐 아니라, 객체의 범위(Scope)를 정의할 수 있다.

설명

Spring 프레임워크는 6개의 Scope를 지원하며, 이 중 4개의 Scope는 Web-aware ApplicationContext를 사용하는 경우에만 사용할 수 있다. 또한, 사용자 정의 범위를 생성할 수도 있다.

Scope설명
singleton하나의 Bean 정의에 대해서 Spring IoC Container 내에 단 하나의 객체만 존재한다.
prototype하나의 Bean 정의에 대해서 다수의 객체가 존재할 수 있다.
request하나의 Bean 정의에 대해서 하나의 HTTP request 생명주기 안에 단 하나의 객체만 존재한다. 즉, 각각의 HTTP Request는 자신만의 객체를 가진다. web-aware Spring ApplicationContext 안에서만 유효하다.
session하나의 Bean 정의에 대해서 하나의 HTTP Session 생명주기 안에 단 하나의 객체만 존재한다. web-aware Spring ApplicationContext 안에서만 유효하다.
application하나의 Bean 정의에 대해서 하나의 ServletContext 생명주기 안에 단 하나의 객체만 존재한다. web-aware Spring ApplicationContext 안에서만 유효하다.
websocket하나의 Bean 정의에 대해서 하나의 Websocket 생명주기 안에 단 하나의 객체만 존재한다. web-aware Spring ApplicationContext 안에서만 유효하다.

The singleton scope

Bean이 singleton인 경우, 단지 하나의 공유 객체만 관리된다.

The singleton scope

Singleton scope은 Spring의 기본(default) scope이다.

<bean id="accountService" class="com.something.DefaultAccountService"/>
 
<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>

The prototype scope

Singleton이 아닌 prototype scope의 형태로 정의된 bean은 필요한 매 순간 새로운 bean 객체가 생성된다.

The prototype scope

<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>

Prototype scope을 사용할 때 염두에 두고 있어야 할 것이 있다.
Spring은 prototype bean의 전체 생명주기를 관리하지 않는데 Container는 객체화하고, 값을 설정하고, 다른 prototype 객체와 조립하여 Client에게 전달한 후 더 이상 prototype 객체를 관리하지 않는다.
즉, scope에 관계없이 초기화(initialization) 생명주기 callback 메소드가 호출되는 반면에, prototype의 경우 파괴(destruction) 생명주기 callback 메소드는 호출되지 않는다.
이것은 client 코드가 prototype 객체를 clean up하고 prototype 객체가 들고 있던 리소스를 해제하는 책임을 가진다는 것을 의미한다.

Prototype bean에 종속적인 singleton bean(Singleton beans with prototype-bean dependencies)

이 문제는 메소드 삽입(Method Injection)에서 다루고 있다.

기타 scopes(The other scopes)

request, session, application, websocket scope들은 반드시 web-based 어플리케이션에서 사용할 수 있다.

기본 Web 설정(Initial web configuration)

request, session, application, websocket scope을 사용하기 위해서는 추가적인 설정이 필요하다. 추가 설정은 사용할 Servlet 환경에 따라 달라진다.
만약 Spring Web MVC 안에서 bean에 접근할 경우, 즉 Spring DispatcherServlet 또는 DispatcherPortlet에서 처리되는 요청인 경우, 별도의 추가 설정은 필요없다.( DispatcherServlet과 DispatcherPortlet은 이미 모든 관련있는 상태를 제공한다.)
만약 Servlet 2.4+ web Container를 사용하고, JSF나 Struts 등과 같이 Spring의 DispatcherServlet의 외부에서 요청을 처리하는 경우, 다음 javax.servlet.ServletRequestListener를 ‘web.xml’ 파일에 추가해야 한다.

<web-app>
   ...
   <listener>
       <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
   </listener>
   ...
</web-app>

만약 다른 오래된 web Container(Servlet 2.3)를 사용한다면, 제공되는 javax.servlet.Filter 구현체를 사용해야 한다.(filter mapping은 web 어플리케이션 설정에 따라 달라질 수 있으므로, 적절히 수정해야 한다.)

<web-app>
   ..
   <filter>
       <filter-name>requestContextFilter</filter-name>
       <filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
   </filter>
   <filter-mapping>
       <filter-name>requestContextFilter</filter-name>
       <url-pattern>/*</url-pattern>
   </filter-mapping>
   ...
</web-app>

The request scope

<bean id="loginAction" class="com.foo.LoginAction" scope="request"/>

위 정의에 따라, Spring Container는 모든 HTTP request에 대해서 ’loginAction’ bean 정의에 대한 LoginAction 객체를 생성할 것이다. 즉, ’loginAction’ bean은 HTTP request 수준에 한정된다(scoped). 요청에 대한 처리가 완료되었을 때, 한정된(scoped) bean도 폐기된다.

The session scope

<bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>

위 정의에 따라, Spring Container는 하나의 HTTP Session 일생동안 ‘userPreferences’ bean 정의에 대한 UserPreferences 객체를 생성할 것이다. 즉, ‘userPreferences’ bean은 HTTP Session 수준에 한정된다(scoped). HTTP Session이 폐기될 때, 한정된(scoped) bean로 폐기된다.

The application scope

<bean id="appPreferences" class="com.foo.AppPreferences" scope="application"/>

위 정의에 따라, Spring Container는 전체 Web Application에 대해 ‘appPreferences’ bean 정의에 대한 AppPreferences 객체를 생성할 것이다. singleton scope와 비슷하지만 bean의 scope와 관련하여 매우 중요한 차이가 있는데, bean이 application 범위인 경우 bean의 동일한 instance는 동일한 ServletContext에서 실행되는 여러 서블릿 기반 애플리케이션에서 공유되는 반면 singleton 범위의 bean은 단일 애플리케이션 컨텍스트로만 범위가 지정된다.

The websocket scope

<bean id="appPreferences" class="com.foo.AppPreferences" scope="websocket"/>

WebSocket scope bean은 WebSocket 세션 속성에 저장된다. 그리고 전체 WebSocket 세션 동안 해당 bean에 액세스할 때마다 bean의 동일한 instance가 반환된다.

한정적 bean에 대한 종속성(Scoped beans as dependencies)

HTTP request 또는 Session에 한정적인(scoped) bean을 정의하는 것은 꽤 괜찮은 기능이지만 Spring IoC Container가 제공하는 핵심 기능은 객체를 생성하는 것 뿐만 아니라 엮어준다는 것이다. 만약 HTTP request에 한정적인(scoped) bean을 다른 bean에 주입하기를 원한다면, 한정적(scoped) bean 대신에 AOP Proxy를 주입해야 한다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:aop="http://www.springframework.org/schema/aop"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">
 
   <!-- a HTTP Session-scoped bean exposed as a proxy -->
   <bean id="userPreferences" class="com.foo.UserPreferences" scope="session">
       <!-- this next element effects the proxying of the surrounding bean -->
       <aop:scoped-proxy/>
   </bean>
 
   <!-- a singleton-scoped bean injected with a proxy to the above bean -->
   <bean id="userService" class="com.foo.SimpleUserService">
       <!-- a reference to the proxied 'userPreferences' bean -->
       <property name="userPreferences" ref="userPreferences"/>
   </bean>
</beans>

Proxy를 생성하기 위해서 <aop:scoped-proxy/> element를 scoped bean 정의에 추가해야 한다(CGLIB 라이브러리도 classpath에 추가해야 한다).

참고자료

1.5 - Customizing the nature of a Bean

Spring에서 Bean의 라이프사이클 관리를 위해 InitializingBeanDisposableBean 인터페이스를 구현할 수 있다. 컨테이너는 빈이 초기화될 때 afterPropertiesSet() 메서드를, 소멸될 때 destroy() 메서드를 호출하여 특정 작업을 수행하도록 한다.

Customizing the nature of a bean

개요

컨테이너의 빈 라이프사이클 관리와 상호 작용하기 위해 Spring InitializingBean 및 DisposableBean 인터페이스를 구현할 수 있는데, 컨테이너는 전자의 경우 afterPropertiesSet()을 호출하고 후자의 경우 destroy()를 호출하여 빈이 초기화 및 소멸될 때 특정 작업을 수행하도록 한다.

설명

Lifecycle callbacks

Spring Framework는 Container 내부의 bean의 행동을 변화시길 수 있는 다양한 callback interface를 제공한다.

객체화 callbacks(Initialization callbacks)

org.springframework.beans.factory.InitializingBean interface를 구현하면 bean에 필요한 모든 property를 설정한 후, 초기화 작업을 수행한다. InitializingBean interface는 다음 메소드를 명시하고 있다.

void afterPropertiesSet() throws Exception;

일반적으로, InitializingBean interface의 사용을 권장하지 않는다. 왜냐하면 code가 불필요하게 Spring과 결합되기(couple) 때문이다. 대안으로, bean 정의는 초기화 메소드를 지정할 수 있다. XML 기반 설정의 경우, ‘init-method’ attribute를 사용한다.

<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
public class ExampleBean {
   public void init() {
       // do some initialization work
   }
}

위 예제는 아래 예제와 같다.

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements InitializingBean {
   @Override
   public void afterPropertiesSet() {
       // do some initialization work
   }
}

파괴 callbacks(Destruction callbacks)

org.springframework.beans.factory.DisposableBean interface를 구현하면, Container가 파괴될 때 bean이 callback를 받을 수 있다. DisposableBean interface는 다음 메소드를 명시하고 있다.

void destroy() throws Exception;

일반적으로, DisposableBean interface의 사용을 권장하지 않는다. 왜냐하면 code가 불필요하게 Spring과 결합되기(couple) 때문이다. 대안으로, bean 정의는 초기화 메소드를 지정할 수 있다. XML 기반 설정의 경우, ‘destroy-method’ attribute를 사용한다.

<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>
public class ExampleBean {
   public void cleanup() {
       // do some destruction work (like releasing pooled connections)
   }
}

위 예제는 아래 예제와 같다.

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements DisposableBean {
   @Override
   public void destroy() {
       // do some destruction work (like releasing pooled connections)
   }
}

기본 객체화 및 파괴 메소드(Default initialization & destroy methods)

Spring Container는 모든 bean에 대해서 같은 이름의 초기화 및 파괴 메소드를 지정할 수 있다.

public class DefaultBlogService implements BlogService {
   private BlogDao blogDao;

   public void setBlogDao(BlogDao blogDao) {
       this.blogDao = blogDao;
   }

   // this is (unsurprisingly) the initialization callback method
   public void init() {
       if (this.blogDao == null) {
           throw new IllegalStateException("The [blogDao] property must be set.");
       }
   }
}
<beans default-init-method="init">
   <bean id="blogService" class="com.foo.DefaultBlogService">
       <property name="blogDao" ref="blogDao" />
   </bean>
</beans>

<beans/> element의 ‘default-init-method’ attribute를 이용하여 기본 객체화 callback 메소드를 지정할 수 있다. 파괴 callback 메소드의 경우 ‘default-destroy-method’ attribute를 이용하여 지정할 수 있다.

<bean/> element에 ‘init-method’, ‘destroy-method’ attribute가 정의되어 있는 경우, 기본값은 무시된다.

생명주기 메커니즘 병합

Spring Framework에서는 3가지 방식의 생명주기 메커니즘이 존재한다: InitialzingBean과 DisposableBean interface; 맞춤 init()과 destroy() 메소드; 그리고 @PostConstruct and @PreDestroy annotations

만약 서로 다른 생명주기 메커니즘을 같이 사용할 경우, 개발자는 적용되는 순서를 알고 있어야 한다. 객체화 메소드의 순서는 다음과 같다.

  1. @PostConstruct annotation이 있는 메소드

  2. InitializingBean callback interface에 정의된 afterPropertiesSet()

  3. 맞춤 init() 메소드

파괴 메소드의 호출 순서는 다음과 같다.

  1. @PreDestroy annotation이 있는 메소드

  2. DisposableBean callback interface에 정의된 destroy()

  3. 맞춤 destroy() 메소드

시작 및 종료 callbacks(Startup and Shutdown Callbacks)

Lifecycle 인터페이스는 자체 수명 주기 요구 사항(예: 일부 백그라운드 프로세스 시작 및 중지)이 있는 모든 객체에 대해 필수 메서드를 정의할 수 있다.

public interface Lifecycle {
   void start();
 
   void stop();
 
   boolean isRunning();
}

모든 Spring 관리 객체는 Lifecycle 인터페이스를 구현할 수 있다. 그런 다음 애플리케이션 컨텍스트 자체가 시작 및 중지 신호를 수신하면(예: 런타임에 중지/재시작 시나리오의 경우) 해당 컨텍스트 내에 정의된 모든 Lifecycle 구현으로 해당 호출을 캐스케이드한다.

public interface LifecycleProcessor extends Lifecycle {
   void onRefresh();
 
   void onClose();
}

참고자료

1.6 - Bean Definition Profiles

Spring Framework 3.1부터 추가된 Bean의 Profile은 동일한 id의 bean을 여러 개 정의하고, 활성화된 Profile에 따라 해당 bean이 Runtime 시에 동작하도록 하는 기능이다. 주로 개발 환경과 운영 환경에서 Profile 설정을 변경해 Spring Container에서 서로 다른 Bean을 적용하는 데 사용된다. Profile을 설정할 때는 반드시 활성화해야 하며, 활성화하지 않으면 NoSuchBeanDefinitionException이 발생한다.

Bean Definition Profiles

개요

Bean의 Profile은 Spring f/w ver. 3.1부터 추가되었으며 동일한 id의 bean을 여러 개 정의하여 사용자의 설정으로 활성화시킨 Profile의 해당 bean이 Runtime시에 동작하도록 하는 기능이다. 보통 개발시점과 운영시점에 bean의 Profile설정 변경만으로 Spring Container에서 Bean적용이 달리 적용되도록 하는데 쓰인다.

Profile설정 시, 반드시 Profile을 활성화해야만 사용가능하다. 만약 Profile만 설정하고 활성화하지 않으면 Exeption(NoSuchBeanDefinitionException)이 발생한다.

설명

아래에서 Profile을 설정하는 방법과 Profile을 활성화(Active Profile)하는 방법에 대하여 알아본다.

Profile 설정 방법

Profile의 설정방법에는 XML설정과 Annotation설정으로 나뉜다.

XML Profile설정

기존의 전형적인 XML Bean설정에 대하여 알아보고 새로 추가된 Profiles설정에 대하여 살펴본다. 동일한 Bean id를 Profile별로 XML에 설정하는 방법에는 XML을 나누는 방법, 하나의 XML에서 관리하는 방법이 있다

A typical XML Configuration

기존에는 Bean id설정은 반드시 유일해야 하며 Bean설정을 변경하기 위해서는 다른 id를 가진 Bean을 새로 설정하거나 동일 Bean의 내부설정을 변경해주어야했다.

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="...">
 
    <bean id="transferService" class="com.bank.service.internal.DefaultTransferService">
        <constructor-arg ref="accountRepository"/>
        <constructor-arg ref="feePolicy"/>
    </bean>
 
    <bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository">
        <constructor-arg ref="dataSource"/>
    </bean>
 
    <bean id="feePolicy" class="com.bank.service.internal.ZeroFeePolicy"/>
 
    <jdbc:embedded-database id="dataSource">
        <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
        <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
    </jdbc:embedded-database>
</beans>

1. Bean Profiles - Xml을 분리하여 설정하는 방법

<transfer-service-config.xml>

아래는 “dataSource” bean을 사용하는 “accountRepository” bean 설정한 XML이다. 어떤 Profile의 bean인지 여부와 상관없이 Spring container기동시점에는 dataSource라는 bean id를 가진 유일한 bean을 가져오기 때문에 이전 설정과 똑같이 bean id값만 쓰면 된다.

<beans ...>
    <bean id="transferService" ... />
 
    <bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository">
        <constructor-arg ref="dataSource"/>
    </bean>
 
    <bean id="feePolicy" ... />
</beans>

<standalone-datasource-config.xml>

개발시점에 사용하는 “dataSource” bean을 정의하는 XML. Profile명은 “dev"로 정의하고 있으며 Embedded DB를 DataSource로 설정하고 있다. “dev” Profile을 활성화시키면 해당Bean이 동작한다.

<beans profile="dev">
    <jdbc:embedded-database id="dataSource">
        <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
        <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
    </jdbc:embedded-database>
</beans>

<jndi-datasource-config.xml>

운영시점에 사용하는 “dataSource” bean을 정의하는 XML. Profile명은 “production"으로 정의하고 있으며 JDNI를 DataSource로 설정하고 있다. “production” Profile을 활성화시키면 해당 Bean이 동작한다.

<beans profile="production">
    <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>

2. Bean Profiles - XML을 한 파일에서 설정

<beans> element를 하나의 XML파일에서 profile값과 함께 여러 번 정의할 수도 있다.

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">
 
    <bean id="transferService" class="com.bank.service.internal.DefaultTransferService">
        <constructor-arg ref="accountRepository"/>
        <constructor-arg ref="feePolicy"/>
    </bean>
 
    <bean id="accountRepository" class="com.bank.repository.internal.JdbcAccountRepository">
        <constructor-arg ref="dataSource"/>
    </bean>
    <bean id="feePolicy" class="com.bank.service.internal.ZeroFeePolicy"/>
    <beans profile="dev">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
            <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
        </jdbc:embedded-database>
    </beans>
    <beans profile="production">
        <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
    </beans>
</beans>

Annotation Profile설정

Profile의 Annotation설정은 @Profile을 통해서 설정 가능하다. @Configuration과 함께 @Profile(“Profile명”)을 클래스에 쓰게 되면 내부 메소드에 붙은 @Bean을 통해 Bean들이 등록된다.

기존 @Configuration class에 대하여 살펴보고 @Profile을 통해 Bean을 설정하는 방법에 대하여 살펴본다.

A typical @Configuration Class

Class위에 @Configuration을 붙이면 @Bean과 함께 정의한 메소드 명이 bean id로 등록된다.

@Configuration
public class TransferServiceConfig {
 
    @Bean
    public TransferService transferService() {
        return new DefaultTransferService(accountRepository(), feePolicy());
    }
 
    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource());
    }
 
    @Bean
    public FeePolicy feePolicy() {
        return new ZeroFeePolicy();
    }
 
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }
}
@Profile 설정

위에서 살펴봤던 Bean Profile XML 설정을 Annotation(@Profile)으로 설정하면 다음과 같다. @Profile은 XML의 beans profile의 설정과 똑같이 동작하며 @Bean은 XML의 bean설정과 매칭된다.

다음은 “dev” Profile을 정의했을 때의 Java코드이다. Profile명이 dev인 “dataSource” Bean을 정의하고 있다.

@Configuration
@Profile("dev")
public class StandaloneDataConfig {
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }
}

다음은 “production” Profile을 정의했을 때의 Java코드이다. Profile명이 production인 “dataSource” Bean을 정의하고 있다.

@Configuration
@Profile("production")
public class JndiDataConfig {
 
    @Bean
    public DataSource dataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

Profile 활성화 (Active Profile)

설정한 Profile을 활성화 하는 방법에는 선언적인 Profile활성화, Java코드를 통한 Profile활성화가 있다.

1. 선언적인 Profile 활성화(web.xml, 환경변수, 프로퍼티 등..)

web.xml 또는 JVM실행 시 환경변수, Property값 등으로 Profile을 활성화시킬 수 있다. Java코드를 통해서도 가능하나 실행환경이 war파일로 제공되거나 배포시점의 경우에는 관리의 불편함이 있으므로 web.xml을 통한 Profile활성화를 추천한다.

<web.xml로 Profile활성화>

<servlet>
      <servlet-name>dispatcher</servlet-name>
      <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
      <init-param>
          <param-name>spring.profiles.active</param-name>
          <param-value>production</param-value>
      </init-param>
  </servlet>

<JVM실행 시 환경변수로 Profile활성화>

java -Dspring.profiles.active="production"

2. Java 코드를 통한 Profile 활성화

Spring 3.1부터 Environment라는 인터페이스를 통해서 해당프로파일을 활성화시킬 수 있다.

<XML로 profile을 설정한 경우, Java코드로 Profile활성화> Profile명이 dev로 설정되어있는 bean을 활성화시킨다. GenericXmlApplicationContext에서 가져온 Environment를 통해 setActiveProfiles메소드로 Profile을 활성화시키고 해당 configuration xml을 로딩한다.

다음 예시에서는 Profile명이 production로 설정되어있는 bean이 활성화되며 Profile명이 dev으로 설정되어있는 bean은 Skip된다.

GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.getEnvironment().setActiveProfiles("production");
ctx.load("classpath:/com/bank/config/xml/*-config.xml");
ctx.refresh();

<@Profile로 Profile을 설정한 경우, Java코드로 Profile활성화 > 활성화하고자하는 Profile명을 setActiveProfiles메소드로 활성화시키고 “com.bank.config.code"패키지 내의 모든 @Configuration class를 스캔한다. Profile이 dev로 설정되어있는 bean이 활성화되며 Profile이 Production으로 설정되어있는 bean은 Skip된다.

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("dev");
ctx.scan("com.bank.config.code"); // find and register all @Configuration classes within
ctx.refresh();

3. JUnit4테스트의 @ActiveProfile 어노테이션을 통한 Profile활성화

JUnit4에서 Test class에 @ActiveProfile을 붙임으로써 Profile활성화가 가능하다. @ContextConfiguration과 함께 쓰며, @ActiveProfile뒤에 Profile명을 붙이면 해당 Profile이 활성화된다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader=AnnotationConfigContextLoader.class)
@ActiveProfiles("annotationProfile")
public class SpringAnnotationProfileTest {
}

참고자료

1.7 - Bean Definition Inheritance

Bean 정의는 설정 정보를 포함하며, 자식 bean 정의는 부모 bean 정의로부터 설정 정보를 상속받는다. 자식 bean 정의는 부모로부터 상속받은 설정 정보를 필요에 따라 덮어쓰거나 추가할 수 있다.

Bean definition inheritance

개요

Bean 정의는 많은 양의 설정 정보를 포함하고 있다. 자식 bean 정의는 부모 bean 정의로부터 설정 정보를 상속받은 bean 정의를 의미한다. 자식 bean 정의는 필요에 따라 부모 bean 정의로부터 상속받은 설정 정보를 덮어쓰거나 추가할 수 있다.

설명

XML 기반 설정에서는 자식 bean 정의에 ‘parent’ attribute를 사용하여 상속관계를 정의할 수 있다.

<bean id="inheritedTestBean" abstract="true"
       class="org.springframework.beans.TestBean">
   <property name="name" value="parent"/>
   <property name="age" value="1"/>
</bean>
 
<bean id="inheritsWithDifferentClass"
       class="org.springframework.beans.DerivedTestBean"
       parent="inheritedTestBean" init-method="initialize">
   <property name="name" value="override"/>
   <!-- the age property value of 1 will be inherited from  parent -->
</bean>

자식 bean 정의는 bean class가 명기되어 있지 않을 경우, 부모 bean 정의의 값을 사용한다. 만약 자식 bean 정의에 bean class가 명기되어 있는 경우, 자식 bean 정의의 bean class는 부모 bean 정의의 모든 property 값을 받아들일 수 있어야 한다.

자식 bean 정의는 부모 bean 정의의 생성자 argument 값, property 값, 그리고 메소드 덮어씀을 상속받는다. 만약 init-method, destroy-method, static factory 메소드 설정이 명기되어 있을 경우, 부모의 설정을 덮어쓴다.

다음 설정은 항상 자식 bean 정의의 값을 따른다: depends on, autowire mode, dependency check, singleton, scope, lazy init.

부모 bean 정의는 abstract attribute를 사용하여 abstract로 설정할 수 있다. 이 경우, 부모 bean 정의는 class를 지정하지 않는다.

<bean id="inheritedTestBeanWithoutClass" abstract="true">
   <property name="name" value="parent"/>
   <property name="age" value="1"/>
</bean>
 
<bean id="inheritsWithClass" class="org.springframework.beans.DerivedTestBean"
       parent="inheritedTestBeanWithoutClass" init-method="initialize">
   <property name="name" value="override"/>
   <!-- age will inherit the value of 1 from the parent bean definition-->
</bean>

부모 bean 정의는 완전하지 않기 때문에 객체화 될 수 없다.

참고자료

1.8 - Container Extension Points

Spring Framework의 IoC 컴포넌트는 확장을 염두에 두고 설계되었다. 일반적으로 개발자가 BeanFactory나 ApplicationContext 구현 클래스를 상속받을 필요는 없으며, Spring IoC Container는 통합 인터페이스의 구현체를 통해 확장할 수 있다.

Container extension points

개요

Spring Framework의 IoC 컴포넌트는 확장을 고려하여 설계되었다. 일반적으로 어플리케이션 개발자가 다양한 BeanFactory 또는 ApplicationContext 구현 클래스를 상속받을 필요는 없다.
Spring IoC Container는 특별한 통합 interface의 구현체를 삽입하여 확장할 수 있다.

설명

BeanPostProcessors를 사용한 확장(Customizing beans using BeanPostProcessors)

BeanPostProcessors interface는 다수의 callback 메소드를 정의하고 있는데, 어플리케이션 개발자는 이들 메소드를 구현함으로써 자신만의 객체화 로직(instantiation logic), 종속성 해결 로직(dependency-resolution logic) 등을 제공할 수 있다.
org.springframework.beans.factory.config.BeanPostProcessor interface는 두개의 callback 메소드로 구성되어 있다. 특정 class가 Container에 post-processor로 등록되면, post-processor는 Container에서 생성되는 각각의 bean 객체에 대해서, Container 객체화 메소드 전에 callback을 받는다.
중요한 것은 BeanFactory는 post-processor를 다루는 방식에 있어서 ApplicationContext와는 조금 다르다. ApplicationContext는 BeanPostProcessor interface를 구현한 bean을 자동적으로 인식하고 post-processor로 등록한다. 하지만 BeanFactory 구현을 사용하면 post-processor는 다음과 같이 명시적으로 등록되어야 한다.

ConfigurableBeanFactory factory = new XmlBeanFactory(...);
 
// now register any needed BeanPostProcessor instances
MyBeanPostProcessor postProcessor = new MyBeanPostProcessor();
factory.addBeanPostProcessor(postProcessor);
 
// now start using the factory

명시적인 등록은 불편하기 때문에 대부분의 Spring 기반 어플리케이션에서는 순수 BeanFactory 구현보다는 ApplicationContext 구현을 사용한다.

Example: Hello World, BeanPostProcessor-style

본 예제는 올바른 예는 아니지만, 기본적인 사용 방법을 보여주기 위함이다.

package scripting;

import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.BeansException;

public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {
   // simply return the instantiated bean as-is
   public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
       return bean; // we could potentially return any object reference here...
   }

   public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
       System.out.println("Bean '" + beanName + "' created : " + bean.toString());
       return bean;
   }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:lang="http://www.springframework.org/schema/lang"
   xsi:schemaLocation=" http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/lang
       http://www.springframework.org/schema/lang/spring-lang.xsd">
 
   <lang:groovy id="messenger" script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
       <lang:property name="message" value="Fiona Apple Is Just So Dreamy."/> 
   </lang:groovy>
 
   <!-- 
   when the above bean ('messenger') is instantiated, this custom
   BeanPostProcessor implementation will output the fact to the system console
   -->
   <bean class="scripting.InstantiationTracingBeanPostProcessor"/>
</beans>

InstantiationTracingBeanPostProcessor는 단순히 정의된다. 비록 이름을 가지고 있지는 않지만 bean이기 때문에 다른 bean과 같이 종속성은 삽입될 수 있다.

BeanFactoryPostProcesors를 사용한 확장(Customizing configuration metadata with BeanFactoryPostProcessors)

org.springframework.beans.factory.config.BeanFactoryPostProcessor는 BeanPostProcessor와 의미적으로 비슷하지만, 큰 차이점 중 하나는 BeanFactoryPostProcessors는 bean 설정 메타정보를 처리한다는 것이다. Spring IoC Container는 BeanFactoryPostProcessors가 설정 메타정보를 읽고, Container가 실제로 bean을 객체화 하기 전에 그 정보를 변경할 수 있도록 허용한다.

Bean factory post-processor는 BeanFactory의 경우 수동으로 실행되고, ApplicationContext의 경우 자동으로 실행된다.

BeanFactory에서는 다음과 같이 수동으로 실행한다.

XmlBeanFactory factory = new XmlBeanFactory(new FileSystemResource("beans.xml"));
 
// bring in some property values from a Properties file
PropertyPlaceholderConfigurer cfg = new PropertyPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));
 
// now actually do the replacement
cfg.postProcessBeanFactory(factory);

Example: the PropertyPlaceholderConfigurer

PropertyPlaceholderConfigurer는 BeanFactory 정의로부터 property 값을 분리하기 위해 사용한다. 분리된 값은 Java Properties 형식으로 작성된 다른 파일로 분리된다. 이 방식은 주 XML 설정 파일을 변경하지 않고, 환경 변수 등을 변경할 때 유용하다.(예를 들어 database URLs, 사용자명, 패스워드 등)

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
   <property name="locations">
       <value>classpath:com/foo/jdbc.properties</value>
   </property>
</bean>
 
<bean id="dataSource" destroy-method="close" class="org.apache.commons.dbcp.BasicDataSource">
   <property name="driverClassName" value="${jdbc.driverClassName}"/>
   <property name="url" value="${jdbc.url}"/>
   <property name="username" value="${jdbc.username}"/>
   <property name="password" value="${jdbc.password}"/>
</bean>

위 설정의 실제 값은 아래와 같다.

jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root

만약 Spring 2.5부터 지원되는 context namespace를 사용하면 다음과 같이 설정할 수 있다.

<context:property-placeholder location="classpath:com/foo/jdbc.properties"/>

PropertyPlaceholderConfigurer는 사용자가 지정한 Properties 파일 뿐 아니라 만약 지정한 property가 없을 경우, Java System properties도 검사할 수 있다. 이 기능은 systemPropertiesMode 설정을 통해 조절할 수 있다.

Example: the PropertyOverrideConfigurer

PropertyOverrideConfigurer는 또다른 bean factory post-processor로 PropertyPlaceholderConfigurer와 비슷하다. 하지만 PropertyPlaceholderConfigurer와는 반대로 원본 설정은 bean properties로 기본(default) 값을 가지거나 전혀 값을 가지지 않을 수 있다. 만약 Properties 파일이 특정 bean property를 위한 값을 가지고 있지 않을 경우, 기본 context definition이 사용된다. Properties 파일 설정의 각 줄은 다음과 같은 형식이다.

beanName.property=value

Spring 2.5부터 지원되는 context namespace를 사용하면 다음과 같이 설정할 수 있다.

<context:property-override location="classpath:override.properties"/>

FactoryBeans를 사용한 확장(Customizing instantiation logic using FactoryBeans)

org.springframework.beans.factory.FactoryBean interface를 구현한 객체는 스스로 fatory가 된다. FactoryBean interface는 Spring IoC Container에 객체화 로직을 삽입할 수 있는 방법이다. 만약 복잡한 객체화 코드를 가지고 있어 장황한 XML 설정보다는 Java로 직접 표현하는 것이 더 좋은 경우, 객체화 코드를 가지고 있는 FactoryBean를 생성하여 Container에서 삽입할 수 있다.

FactoryBean interface는 다음 3가지 메소드를 제공한다.

  • Object getObject(): 생성한 객체를 return한다. 생성된 객체는 공유될 수도 있다.

  • boolean isSingleton(): 만약 FactoryBean이 singleton을 리턴하면 true, 아니면 false 이다.

  • Class getObjectType(): getObject() 메소드에 의해 객체의 타입을 리턴하는데 미리 알 수 없는 경우에는 null을 리턴한다.

Container에게 FactoryBean이 생성한 객체가 아닌 FactoryBean 그 자체를 요구하는 경우도 있다. 이런 경우, BeanFactory의 getBean 메소드를 호출할 때 bean id 앞에 ‘&‘를 붙이면 된다.

참고자료

1.9 - The ApplicationContext

org.springframework.context 패키지는 ApplicationContext를 통해 BeanFactory를 확장하여 애플리케이션 프레임워크에 맞는 추가 기능을 제공하며, 대부분의 경우 ContextLoader 등으로 자동 인스턴스화된다.

The ApplicationContext

개요

org.springframework.context 패키지는 BeanFactory 인터페이스를 확장하는 ApplicationContext 인터페이스를 추가하고, 다른 인터페이스를 확장하여 보다 애플리케이션 프레임워크 지향적인 스타일로 추가 기능을 제공한다.
많은 사람들이 ApplicationContext를 완전히 선언적인 방식으로 사용하며, 프로그래밍 방식으로 생성하지 않고 ContextLoader와 같은 지원 클래스에 의존하여 Java EE 웹 애플리케이션의 정상적인 시작 프로세스의 일부로 ApplicationContext를 자동으로 인스턴스화한다.

설명

ApplicationContext는 BeanFactory를 확장한 것으로 BeanFactory의 기능 외에 아래와 같은 기능을 제공한다.

  • MessageSource : i18n-sytle로 메시지를 access할 수 있도록 지원한다.
  • Access to resources : URL, File 등과 같은 자원을 쉽게 access할 수 있도록 지원하다.
  • Event propagation : ApplicationListener interface를 구현한 bean에게 Event를 전달한다.
  • Loading of multiple (hierarchical) contexts : 계층 구조의 context를 지원함으로써, 어플리케이션의 웹 레이어 등과 같은 특정 레이어에만 집중적인 context를 작성할 수 있다.

BeanFactory or ApplicationContext?

아주 특별한 이유가 없는 한 ApplicationContext를 사용하는 것이 좋다. 다음은 BeanFactory와 ApplicationContext의 기능 비교표이다.

FeatureBeanFactoryApplicationContext
Bean 객체화/엮음YesYes
BeanPostProcessor 자동 등록NoYes
BeanFactoryPostProcessor 자동 등록NoYes
편리한 MessageSource 접근(for i18n)NoYes
ApplicationEvent 발송NoYes

MessageSources를 사용한 국제화(Internationalization using MessageSources)

본 장의 내용은 Resource를 참조한다.

Event

ApplicationContext는 Event 처리를 위해 ApplicationEvent, ApplicationListener interface를 제공한다. ApplicationListener interface를 구현한 bean은 ApplicationContext에 발생한 모든 ApplicationEvent를 전달받는다. Spring이 제공하는 표준 event는 다음과 같다.

Event설명
ContextRefreshedEventApplicationContext가 초기화된거나 refresh될 때 발생한다(refresh하기 위해서 ConfigurableApplicationContext interface의 refresh() 메소드를 사용한다). “초기화”라는 단어는, 모든 bean이 load되었고, post-processor bean이 탐지되어 활성화되었으며, singleton 객체가 선객체화되어, ApplicationContext 객체가 사용가능한 상태에 있다는 것을 의미한다. refresh는 context가 닫혀지지 않은 한, 여러번 발생할 수 있으며, ApplicationContext가 “hot” refresh를 지원해야한다 (XmlWebApplicationContext는 “hot” refresh를 지원하지만, GenericApplicationContext는 지원하지 않는다).
ContextStartedEventApplicationContext가 시작될 때 발생한다(시작하기 위해서 ConfigurableApplicationContext interface의 start() 메소드를 사용한다). “시작됨(Started)“이란 단어는, 모든 Lifecycle bean이 명시적인 시작 신호를 받았음을 의미한다. 이 event는 일반적으로 명시적인 정지(stop) 후에, 재시작하기 위해서 사용되지만, 자동시작(autostart)로 설정되지 않은 컴포넌트를 시작하기 위해서도 사용된다
ContextStoppedEventApplicationContext가 정지할 때 발생한다(정지하기 위해서 ConfigurableApplicationContext interface의 stop() 메소드를 사용한다). “정지됨(Stopped)“란 단어는, 모든 Lifecycle bean이 명시적인 정지 신호를 받았음을 의미한다. 정지된 context는 start() 호출을 통해 재시작될 수 있다.
ContextClosedEventApplicationContext가 닫혔을 때 발생한다(닫기 위해서 ConfigurableApplicationContext interface의 close() 메소드를 사용한다). “닫힘(Closed)“란 단어는, 모든 singleton bean이 파괴되었음을 의미한다. 닫힌 context는 생명주기의 끝에 도달한 것으로, refresh 되거나 재시작될 수 없다.
RequestHandledEvent웹에 특화된 event로서, HTTP request가 처리되었음을 알린다(request가 종료된 후에 발송된다). Spring의 DispatcherServlet를 사용하는 웹 어플리케이션인 경우에만 사용할 수 있다.

새로운 event를 구현하는 것도 어렵지 않다. ApplicationContext interface를 구현한 새로운 event 객체를 ApplicationContext의 publishEvent() 메소드를 통해 발행하면 된다. publishEvent() 메소드는 모든 listener가 event 처리를 마칠때까지 block 상태로 있게 된다. 게다가 transaction context가 가능하다면, listener가 event를 받았을 때, 발행모듈의 transaction context 내에서 event를 처리한다.

아래는 예제이다.

<bean id="emailer" class="example.EmailBean">
   <property name="blackList">
       <list>
           <value>black@list.org</value>
           <value>white@list.org</value>
           <value>john@doe.org</value>
           </list>
   </property>
</bean>
 
<bean id="blackListListener" class="example.BlackListNotifier">
   <property name="notificationAddress" value="spam@list.org"/>
</bean>
public class EmailBean implements ApplicationContextAware {
   private List blackList;
   private ApplicationContext ctx;

   public void setBlackList(List blackList) {
       this.blackList = blackList;
   }

   public void setApplicationContext(ApplicationContext ctx) {
       this.ctx = ctx;
   }

   public void sendEmail(String address, String text) {
       if (blackList.contains(address)) {
           BlackListEvent event = new BlackListEvent(address, text);
           ctx.publishEvent(event);
           return;
       }
       // send email...
   }
}
public class BlackListNotifier implements ApplicationListener {
   private String notificationAddress;

   public void setNotificationAddress(String notificationAddress) {
       this.notificationAddress = notificationAddress;
   }

   public void onApplicationEvent(ApplicationEvent event) {
       if (event instanceof BlackListEvent) {
           // notify appropriate person...
       }
   }
}

웹 어플리케이션을 위한 편리한 ApplicationContext 객체화(Convenient ApplicationContext instantiation for web applications)

BeanFactory가 프로그램적으로 생성되는 것과 반대로, ApplicationContext 객체는 ContextLoader 등을 사용하여 선언적으로 생성될 수 있다. 물론 ApplicationContext 객체 역시 프로그램적으로 생성할 수 있다.
ContextLoader에는 ContextLoaderListener와 ContextLoaderServlet가 있다. 둘 다 기능적으로는 같지만, listener 버전은 Servlet 2.3 컨테이너에서는 사용할 수 있다.
Servlet 2.4 스팩 이후로, servlet context listener는 웹 어플리케이션을 위한 servlet context가 생성되어 첫번째 요청을 처리할 상태가 된 직후 수행된다(그리고 servlet context가 막 종료되었을 때도 수행된다).
따라서 servlet context listner가 Spring ApplicationContext를 초기화할 최적의 장소이다.
ContextLoaderListener를 사용하여 ApplicationContext를 등록하는 방법은 아래와 같다.

<context-param>
   <param-name>contextConfigLocation</param-name>
   <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>
 
<listener>
   <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
 
<!-- or use the ContextLoaderServlet instead of the above listener
<servlet>
   <servlet-name>context</servlet-name>
   <servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class>
   <load-on-startup>1</load-on-startup>
</servlet>
-->

Listener는 ‘contextConfigLocation’ 파라미터를 검사한다. 만약 존재하지 않으면 기본적으로 /WEB-INF/applicationContext.xml를 사용할 것이다.
만약 ‘contextConfigLocation’ 파라미터 값이 존재할 경우, 미리 정해놓은 구분자(comma(’,’), semicolon(’;’), 공백문자(whitespace))를 사용하여 파라미터 문자열을 분리한 후, application context를 찾을 것이다.
Ant-style path 패턴이 지원된다. 예를 들어 /WEB-INF/*Context.xml’은 “WEB-INF” 디렉토리에 존재하는 “Context.xml”로 끝나는 이름을 가진 모든 파일을 의미하고, \
/WEB-INF/**/*Context.xml은 “WEB-INF” 디렉토리 및 하위 디렉토리에 존재하는 “Context.xml”로 끝나는 이름을 가진 모든 파일을 의미한다.

참고자료

1.10 - Annotation-based Configuration

Spring Framework는 의존성 주입을 위해 어노테이션을 사용할 수 있다. Spring 2.0에서는 @Required 어노테이션으로 필수 속성을 강제하는 기능이 도입되었고, Spring 2.5에서는 일반적인 어노테이션 기반 의존성 주입이 가능해졌다. Spring 3.0부터는 JSR-330(Java용 의존성 주입)의 @Inject 및 @Named와 같은 어노테이션도 지원된다.

Annotation-based configuration

개요

Spring Framework는 Spring의 종속성 삽입을 위해 annotation을 사용할 수 있다. Spring 2.0에서는 @Required 어노테이션으로 필수 속성을 강제할 수 있는 기능이 도입되었고 Spring 2.5에서는 이와 동일한 일반적인 접근 방식을 따라 Spring의 의존성 주입을 구동할 수 있게 되었으며, Spring 3.0부터 @Inject 및 @Named와 같이 javax.inject 패키지에 포함된 JSR-330(Java용 의존성 주입) 어노테이션에 대한 지원이 추가되었다.

설명

Spring @Autowired annotation은 자동 엮음과 같은 기능을 제공하지만, 좀 더 세밀한 제어와 넓은 사용성을 제공한다. Spring Framework는 @Resource, @PostConstruct, @PreDestroy 등의 JSR-250 annotation도 지원한다. 이들 annotation을 사용하기 위해서는 Spring Container에 특정 BeanPostProcessors를 등록해야만 한다. 항상 그렇듯이, 이들 BeanPostProcessors가 개별적인 bean 정의로 등록될 수도 있지만, ‘context’ namespace를 사용하여 등록할 수도 있다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:context="http://www.springframework.org/schema/context"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">
 
   <context:annotation-config/>
</beans>

(위 <context:annotation-config/> tag를 사용하면, AutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor, PersistenceAnnotationBeanPostProcessor, RequiredAnnotationBeanPostProcessor를 등록해 준다.)

@Required

@Required annotation은 bean property setter 메소드에 적용된다.

public class SimpleMovieLister {
   private MovieFinder movieFinder;
 
   @Required
   public void setMovieFinder(MovieFinder movieFinder) {
       this.movieFinder = movieFinder;
   }
 
   // ...
}

이 annotation은 단순히 bean property가 설정 시 반드시 설정되어야만 한다는 것을 나타낸다. 즉, bean 정의에 명시적으로 property 값을 선언하거나 자동 엮임을 통해서 설정되어야만 한다는 것을 의미한다. 만약 annotation이 적용된 bean property에 대한 설정이 이루어지지 않는 경우, Container는 exception을 던진다.

@Autowired

@Autowired annotation은 “전통적인” setter 메소드에 적용된다.

public class SimpleMovieLister {
   private MovieFinder movieFinder;
 
   @Autowired
   public void setMovieFinder(MovieFinder movieFinder) {
       this.movieFinder = movieFinder;
   }

   // ...
}

뿐만 아니라, 임의의 이름과 다수의 argument를 가진 메소드에도 적용될 수 있다.

public class MovieRecommender {
   private MovieCatalog movieCatalog;
 
   private CustomerPreferenceDao customerPreferenceDao;
 
   @Autowired
   public void prepare(MovieCatalog movieCatalog, CustomerPreferenceDao customerPreferenceDao) {
       this.movieCatalog = movieCatalog;
       this.customerPreferenceDao = customerPreferenceDao;
   }
 
   // ...
}

또한, @Autowired annotation은 생성자 및 field에도 적용될 수 있다.

public class MovieRecommender {
   @Autowired
   private MovieCatalog movieCatalog;
 
   private CustomerPreferenceDao customerPreferenceDao;
 
   @Autowired
   public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
       this.customerPreferenceDao = customerPreferenceDao;
   }
 
   // ...
}

또한, array 타입의 field나 메소드에 적용함으로써, ApplicationContext에 존재하는 특정 Type의 모든 bean을 field나 메소드에 제공하는 것도 가능한다.

public class MovieRecommender {
 
   @Autowired
   private MovieCatalog[] movieCatalogs;
 
   // ...
}

Typed collections에도 같은 방식이 적용된다.

public class MovieRecommender {
   private Set<MovieCatalog> movieCatalogs;
 
   @Autowired
   public void setMovieCatalogs(Set<MovieCatalog> movieCatalogs) {
       this.movieCatalogs = movieCatalogs;
   }
 
   // ...
}

심지어 typed Map 역시 key 타입이 String인 한 자동 엮임이 가능하다. Map은 기대한 타입의 모든 bean을 value로 갖게 되고, key는 해당하는 bean의 이름이 된다.

public class MovieRecommender {
   private Map<String, MovieCatalog> movieCatalogs;

   @Autowired
   public void setMovieCatalogs(Map<String, MovieCatalog> movieCatalogs) {
       this.movieCatalogs = movieCatalogs;
   }

   // ...
}

기본적으로, 자동 엮임은 대상이 되는 bean이 없을 경우 실패한다. 기본적으로 annotation이 적용된 메소드, 생성자, field는 필수로 간주한다. 아래와 같이 설정하여 기본 행동 방식을 변경할 수 있다.

public class SimpleMovieLister {
   private MovieFinder movieFinder;
 
   @Autowired(required=false)
   public void setMovieFinder(MovieFinder movieFinder) {
       this.movieFinder = movieFinder;
   }
 
   // ...
}

@Autowired annotation은 잘 알려진 “분석가능한 종속성(resolvable dependencies)“에도 사용될 수 있다 : BeanFactory interface, ApplicationContext interface, ResourceLoader interface, ApplicationEventPublisher interface, MessageSource interface(그리고 이들을 상속한 ConfigurableApplicationContext 또는 ResourcePatternResolver interface)는 특별한 설정 없이 자동적으로 해결(resolve)된다.

Qualifier를 사용한 annotation 기반의 자동 엮음(Fine-tuning annotation-based autowiring with qualifiers)

Type을 이용한 자동 엮기는 대상이 다수가 발생할 수 있기 때문에, 선택 시 추가적인 제어가 필요하다. 한 방법으로 Spring의 @Qualifier annotation을 사용할 수 있다. 특정 argument를 qualifier와 관련시킴으로써, 타입을 찾을 대상을 좁히고, 각 argument에 해당하는 대상 bean을 선택할 수 있다.

public class MovieRecommender {
 
   @Autowired
   @Qualifier("main")
   private MovieCatalog movieCatalog;
 
   // ...
}

@Qualifier annotation은 생성자의 argument 및 메소드의 parameter 각각에 적용할 수 있다.

public class MovieRecommender {
 
   private MovieCatalog movieCatalog;
 
   private CustomerPreferenceDao customerPreferenceDao;
 
   @Autowired
   public void prepare(@Qualifier("main") MovieCatalog movieCatalog, CustomerPreferenceDao customerPreferenceDao) {
       this.movieCatalog = movieCatalog;
       this.customerPreferenceDao = customerPreferenceDao;
   }
 
   // ...
}

일치하는 bean 정의는 아래 예제에서 찾을 수 있다. Qualifier “main” 값을 가진 bean이 같은 값의 @Qualifier annotation이 있는 생성자 argument로 엮인다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:context="http://www.springframework.org/schema/context"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">
 
   <context:annotation-config/>
 
   <bean class="example.SimpleMovieCatalog">
       <qualifier value="main"/>
       <!-- inject any dependencies required by this bean -->
   </bean>
 
   <bean class="example.SimpleMovieCatalog">
       <qualifier value="action"/>
       <!-- inject any dependencies required by this bean -->
   </bean>
 
   <bean id="movieRecommender" class="example.MovieRecommender"/>
 
</beans>

대체 수단으로, bean 이름을 qualifier로 간주된다. 즉, qualifier element 대신 bean id가 “main”이라면 동일하게 동작한다. 어쨌든, 이름으로 bean을 찾는게 더 좋지만, @Autowired는 기본적으로 type을 기반으로 동작하고 qualifier는 선택적이다. 즉, 타입으로 bean 대상을 줄인 후에 qualifier 또는 bean name으로 대상을 좁힌다. Qualifier 값은 의미적으로 유일한 bean id를 나타내지는 않는다. 좋은 qualifier 값은 “main”, “EMEA”, “persistent” 등과 같이 bean id가 아닌 특정 컴포넌트의 특징을 표현하는 것이다. Qualifier는 typed collection에도 적용된다.

@Resource

Spring은 field나 bean property setter 메소드에 적용된 JSR-250 @Resource annotation을 사용하여 종속성 삽입을 지원한다. @Resource는 ’name’ attribute를 가지고, Spring은 그 값을 삽입할 bean 이름으로 인식한다.

public class SimpleMovieLister {
   private MovieFinder movieFinder;
 
   @Resource(name="myMovieFinder")
   public void setMovieFinder(MovieFinder movieFinder) {
       this.movieFinder = movieFinder;
   }
}

만약 name이 명식적으로 설정되어 있지 않으면, field나 setter 메소드의 이름으로부터 name 값을 유추해낸다. Field의 경우, field 명과 같다. Setter 메소드의 경우, bean property 이름과 같다. 아래 예제에서는 “movieFinder” bean이 삽입된다.

public class SimpleMovieLister {
   private MovieFinder movieFinder;
 
   @Resource
   public void setMovieFinder(MovieFinder movieFinder) {
       this.movieFinder = movieFinder;
   }
}

@Autowired와 비슷하게, @Resource도 대안으로 bean type으로 대상을 찾는다. 뿐만 아니라 잘 알려진 “resolvable dependencies”로 해결한다. 둘 다 명시적으로 name을 설정하지 않은 경우 적용된다. 다음 예에서 customerPreferenceDao field를 위해서 “customerPreferenceDao” 이름을 가진 bean을 먼저 찾는다. 그 다음으로 CustomerPreferenceDao Type의 bean을 찾는다. “context” field는 알려진 해결가능한 종속성 type인 ApplicationContext에 기반하여 삽입된다.

public class MovieRecommender {
   @Resource
   private CustomerPreferenceDao customerPreferenceDao;
 
   @Resource
   private ApplicationContext context;
 
   public MovieRecommender() {
   }
 
   // ...
}

@PostConstrutor와 @PreDestroy

CommonAnnotationBeanPostProcessor는 @Resource annotation 뿐 아니라 JSR-250 lifecycle annotation 역시 인식한다.

public class CachingMovieLister {
   @PostConstruct
   public void populateMovieCache() {
       // populates the movie cache upon initialization...
   }
 
   @PreDestroy
   public void clearMovieCache() {
       // clears the movie cache upon destruction...
   }
}

참고자료

1.11 - Classpath Scanning for Managed Components

이전의 대부분 예제들은 Spring Container에서 BeanDefinition을 생성하기 위한 설정 메타데이터로 XML을 사용했다. 이전 섹션에서는 소스 레벨의 어노테이션을 사용해 많은 설정 메타데이터를 제공할 수 있음을 보여주었다. 그러나 이 예제들에서도 기본적인 bean 정의는 여전히 XML 파일에 명시적으로 작성되었다. 이번 섹션에서는 classpath를 검색하고 filter를 사용해 대상 컴포넌트(candidate component) 를 검출하는 방법을 소개한다.

Classpath scanning for managed components

개요

본 장의 앞선 대부분의 예제들은 Spring Container 안에서 BeanDefinition을 생성하기 위한 설정 메타데이터를 명기하기 위해서 XML을 사용해왔다. 이전 section Annotaion-based configuration은 source-level annotation을 사용하여 많은 양의 설정 메타데이터를 제공할 수 있음을 보였다. 이들 예제에서도 어쨌든, “base” bean 정의가 XML 파일 안에 명시적으로 정의되었다. 이번 section은 classpath를 검색하고, filter를 통해 검사함으로써, 대상 컴퍼넌트(candidate component) 를 검출하는 방법을 소개한다.

설명

@Component and further stereotype annotations

Spring 2.0부터 Data Access Object(DAO) 등과 같은 repository를 표시하기 위해서 @Repository annotation이 소개되었다. Spring 2.5는 추가적으로 @Component, @Service, @Controller annotation을 제공한다. @Component는 Spring이 관리하는 컴포넌트를 위한 포괄적인 stereotype을 나타낸다. 그리고 @Repository, @Service, @Controller는 좀 더 특별한 사용을 위한 @Component의 일종이다. (각각 persistence, service, presentation layer의 component를 의미한다.)

Auto-detection Components

Spring은 ‘stereotyped’ class를 자동으로 탐지하고 ApplicationContext에 일치하는 BeanDefinition을 등록하는 기능을 제공한다. 아래 두 class는 자동 탐지의 대상이 된다.

@Service
public class SimpleMovieLister {
   private MovieFinder movieFinder;
 
   @Autowired
   public SimpleMovieLister(MovieFinder movieFinder) {
       this.movieFinder = movieFinder;
   }
}
@Repository
public class JpaMovieFinder implements MovieFinder {
   // implementation elided for clarity
}

실제로 위 두 class를 자동 탐지하고 상응하는 bean을 등록하기 위해서는, 아래 예제 XML의 <context:component-scan/> element의 ‘basePackage’가 위 두 class의 공통 부모 package이어야 한다(또는 comma(’,’)로 구분된 list 역시 가능하다).

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:context="http://www.springframework.org/schema/context"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">
 
   <context:component-scan base-package="org.example"/>
 
</beans>

Naming autodetected components

검색 과정에서 component가 자동 탐지되었을 때, bean 이름은 scan하고 있는 BeanNameGenerator 전략에 따라 생성된다. 기본적으로 name 값을 가지고 있는 Spring ‘stereotype’ annotation(@Component, @Repository, Service, Controller)는 상응하는 bean 정의에게 이름을 제공한다. 만약 이들 annotation이 name이 없거나 또 다른 탐지된 component인 경우, 기본 bean name generator는 class 이름의 첫 문자를 소문자로 변환한 값을 return할 것이다. 예를 들어, 아래 예제에서 두개의 component가 탐지되는데 각각의 이름은 ‘myMovieLister’와 ‘movieFinderImpl’이다.

@Service("myMovieLister")
public class SimpleMovieLister {
   // ...
}
@Repository
public class MovieFinderImpl implements MovieFinder {
   // ...
}

Providing a scope for autodetected components

일반적으로 Spring 관리 component는 ‘singleton’이다. 어쨌든 다른 scope이 필요한 경우가 있다. Spring Framework는 @Scope annotation을 제공한다.

@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
   // ...
}

Providing qualifier metadata with annotations

이번 section에서는 자동 엮임의 대상을 찾을 때 상세한 제어를 제공하기 위해 @Qualifier annotation을 사용하는 방법을 설명한다.

@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
   // ...
}

참고자료

1.12 - JSR 330 표준 어노테이션 사용하기

Spring 3.0부터 JSR-330 표준 의존성 주입 어노테이션을 지원하며, 이를 사용하려면 클래스패스에 javax.inject 라이브러리를 추가해야 한다.

JSR 330 표준 어노테이션 사용하기

개요

스프링 3.0부터 JSR-330 표준 어노테이션(의존성 주입)을 지원한다. 이 어노테이션들은 스프링 어노테이션들과 같은 방법으로 스캔된다. 이 어노테이션들을 사용하기 위해서는 클래스패스에 관련 jar 파일들을 가지고 있어야 한다.
Maven을 사용한다면 Maven Repository(https://mvnrepository.com/artifact/javax.inject/javax.inject/1)에서 javax.inject라는 아티펙트가 제공된다. pom.xml 파일에 아래의 의존성을 추가하여 사용할 수 있다.

<dependency>
   <groupId>javax.inject</groupId>
   <artifactId>javax.inject</artifactId>
   <version>1</version>
</dependency>

설명

1. @Inject 와 @Named 를 이용한 의존성 주입

@Autowired를 대신하여 @javax.inject.Inject를 아래와 같이 사용할 수 있다.

import javax.inject.Inject;
 
public class SimpleMovieLister {
   private MovieFinder movieFinder;
 
   @Inject
   public void setMovieFinder(MovieFinder movieFinder) {
       this.movieFinder = movieFinder;
   }
 
   public void listMovies() {
       this.movieFinder.findMovies(...);
       // ...
   }
}

@Autowired와 같이 @Inject를 필드 수준, 함수 수준, 생성자 인자 수준으로 사용할 수 있고, 주입 지점을 Provider로 선언할 수 있으며 Provider.get() 호출을 통해 근접 범위의 Bean들에 대한 요청 시 접근 또는 다른 Bean에 대한 지연된 접근을 허용할 수 있다.

import javax.inject.Inject;
import javax.inject.Provider;
 
public class SimpleMovieLister {
   private Provider<MovieFinder> movieFinder;
 
   @Inject
   public void setMovieFinder(Provider<MovieFinder> movieFinder) {
       this.movieFinder = movieFinder;
   }
 
   public void listMovies() {
       this.movieFinder.get().findMovies(...);
       // ...
   }
}

주입될 의존성에 대해 지정된 이름을 사용하고자 할 경우에는 아래와 같이 @Named 어노테이션을 사용할 수 있다.

import javax.inject.Inject;
import javax.inject.Named;
 
public class SimpleMovieLister {
   private MovieFinder movieFinder;
 
   @Inject
   public void setMovieFinder(@Named("main") MovieFinder movieFinder) {
       this.movieFinder = movieFinder;
   }
 
   // ...
}

2. @Component 어노테이션과 동일한 표준인 @Named, @ManagedBean 어노테이션

@Component를 대신하여, @javax.inject.Named나 javax.annotation.ManagedBean를 아래와 같이 사용할 수 있다.

import javax.inject.Inject;
import javax.inject.Named;
 
@Named("movieListener")  // @ManagedBean("movieListener") could be used as well
public class SimpleMovieLister {
   private MovieFinder movieFinder;
 
   @Inject
   public void setMovieFinder(MovieFinder movieFinder) {
       this.movieFinder = movieFinder;
   }
 
   // ...
}

일반적으로 컴포넌트에 대한 이름을 명시하지 않고 @Component를 사용할 수 있는데, 아래 예제처럼 @Named도 비슷하게 사용할 수 있다.

import javax.inject.Inject;
import javax.inject.Named;
 
@Named
public class SimpleMovieLister {
   private MovieFinder movieFinder;
 
   @Inject
   public void setMovieFinder(MovieFinder movieFinder) {
       this.movieFinder = movieFinder;
   }
 
   // ...
}

@Named나 @ManagedBean을 사용할 때 아래 예제에서 보여주는 것처럼 스프링 어노테이션과 같이 동일한 방법으로 컴포넌트 탐색이 가능하다.

@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
   // ...
}

3. JSR-330 표준 어노테이션의 제한점

표준 어노테이션으로 작업할 때 아래 표와 같이 일부 중요한 기능이 사용 불가능하다.

Springjavax.inject.*javax.inject 제한 및 비고
@Autowired@Inject@Inject는 required 속성이 없다. 자바 8의 Optional을 대신 사용할 수 있다.
@Component@Named / @ManagedBeanJSR-330은 조합구성을 제공하지 않기 때문에 명명된 컴포넌트만 식별해야 한다.
@Scope(“singleton”)@SingletonJSR-330의 기본범위는 스프링의 prototype 같으나 스프링의 일반 기본값과 일관성을 유지하기 위해 스프링 컨테이너에 선언된 JSR-330 빈은 기본적으로 singleton이다. Singleton이 아닌 다른 범위를 사용하려면 스프링의 @Scope 어노테이션을 사용해야 한다. javax.inject도 @Scope 어노테이션을 제공하지만 자체적 어노테이션을 생성할 때 사용한다.
@Qualifier@Qualifier / @Namedjavax.inject.Qualifier는 사용자 정의 한정자를 만들기 위한 메타 어노테이션으로 스프링의 값이 있는 @Qualifier 같은 구체적인 String으로 된 한정자는 javax.inject.Named로 연결할 수 있습니다.
@Value-동등한 것이 없음
@Required-동등한 것이 없음
@Lazy-동등한 것이 없음
ObjectFactoryProviderjavax.inject.Provider는 짧은 get() 함수명만 있는 스프링의 ObjectFactory의 직접적 대안으로 사용할 수 있으며 스프링의 @Autowired, 어노테이션이 없는 생성자나 Setter 함수와 조합하여 사용될 수 있다.

참고자료

1.13 - Java-based Configuration

Java-based Configuration은 XML 대신 Java 코드를 사용하여 Spring 빈과 애플리케이션 설정을 구성하는 방법이다.

Java-based configuration

개요

Java 코드에서 주석을 사용하여 스프링 컨테이너를 구성하는 방법에 대해 알아본다.

설명

기본 개념 : @Bean, @Configurationl

스프링의 자바 기반 설정에서는 @Configuration 어노테이션 클래스와 @Bean 어노테이션 메소드를 지원한다.
@Bean 어노테이션은 Spring IoC 컨테이너가 관리할 새로운 객체를 인스턴스화하고, 초기화하는데 사용되며, Spring의 XML 설정에서의 <bean/>과 같은 역할을 한다.
@Bean 어노테이션은 붙인 메소드는 스프링 @Component와 함께 사용할 수 있지만, 대체로 @Configuration Bean과 사용한다.
@Configuration 어노테이션은 해당 클래스의 목적이 Bean 설정을 위한 소스임을 나타내며, @Configuration 클래스는 같은 클래스 안에 있는 @Bean 메소드들끼리 서로를 호출하여 Bean 사이의 의존성을 정의할 수 있게 한다.
@Configuration 클래스를 아래와 같이 구성할 수 있다.

@Configuration
public class AppConfig {
   @Bean
   public MyService myService() {
       return new MyServiceImpl();
   }
}

예제의 AppConfig 클래스는 아래의 Spring <bean/> XML과 동일한 역할을 한다.

<beans>
   <bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>

AnnotationConfigApplicationContext를 이용한 스프링 컨테이너 인스턴스화

AnnotationConfigApplicationContext는 다재다능한 ApplicationContext 구현체로, 입력으로 @Configuration 클래스뿐만 아니라 평범한 @Component 클래스와 JSR-330 메타데이터로 어노테이션이 붙은 클래스들도 받아들일 수 있다.
@Configuration 클래스가 입력으로 제공되면 @Configuration 클래스 자체가 Bean 정의로 등록되고, 해당 클래스내의 선언된 모든 @Bean 메서드들도 Bean 정의로 등록된다.
@Component와 JSR-330 클래스들이 제공되면 이 클래스들은 Bean 정의로 등록되고 해당 클래스내에서 필요한 곳에 @Autowired나 @Inject 같은 DI 메타데이터가 사용되었다고 가정한다.

간단한 구성

ClassPathXmlApplicationContext를 인스턴스화 할 때 스프링 XML 파일을 사용하는 방법과 거의 동일하게 AnnotationConfigApplicationContext를 인스턴스화 할 때 @Configuration 클래스들을 입력으로 사용할 것이다.
이를 통해 전혀 XML을 사용하지 않고 스프링 컨테이너를 사용할 수 있다.

public static void main(String[] args) {
   ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
   MyService myService = ctx.getBean(MyService.class);
   myService.doStuff();
}

register(Class…)로 프로그래밍 방식으로 컨테이너 빌드

인수 없는 생성자를 사용하여 AnnotationConfigApplicationContext를 인스턴스화한 다음 register() 메서드를 사용하여 구성할 수 있다.
이 접근 방식은 AnnotationConfigApplicationContext를 프로그래밍 방식으로 빌드할 때 특히 유용하며 아래와 같이 사용할 수 있다.

public static void main(String[] args) {
   AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
   ctx.register(AppConfig.class, OtherConfig.class);
   ctx.register(AdditionalConfig.class);
   ctx.refresh();
   MyService myService = ctx.getBean(MyService.class);
   myService.doStuff();
}

scan(String…)으로 컴포넌트 스캔

@Configuration구성 요소 스캔을 활성화하려면 다음과 같이 구성할 수 있다.

@Configuration
@ComponentScan(basePackages = "com.acme") // 구성요소 스캔을 활성화함
public class AppConfig  {
   // ...
}

경험있는 스프링 사용자들은 다음과 같이 일반적으로 사용되는 스프링의 context: 네임스페이스로 XML을 선언하는데 익숙할 것이다.

<beans>
   <context:component-scan base-package="com.acme"/>
</beans>

위의 예제에서 com.acme 팩키지는 스캔되고 @Component 어노테이션이 붙은 클래스들을 찾고 이러한 클래스를 컨테이너내 스프링 빈 정의로 등록할 것이다.
AnnotationConfigApplicationContext에는 같은 컴포넌트 스캔 기능을 하는 scan(String…) 메서드가 있다.

public static void main(String[] args) {
   AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
   ctx.scan("com.acme");
   ctx.refresh();
   MyService myService = ctx.getBean(MyService.class);
}

AnnotationConfigWebApplicationContext를 사용한 웹 어플리케이션 지원

AnnotationConfigApplicationContext의 WebApplicationContext 변형은 AnnotationConfigWebApplicationContext로 사용할 수 있다.
이 구현체는 스프링 ContextLoaderListener 서블릿 리스너, 스프링 MVC DispatcherServlet 등을 설정할 때 사용할 수 있다.
아래는 전형적인 스프링 MVC 웹 어플리케이션을 설정하는 web.xml의 예제로 contextClass context-param과 init-param의 사용방법을 보여준다.

<web-app>
   <!-- 기본 XmlWebApplicationContext 대신 AnnotationConfigWebApplicationContext를 사용하는 ContextLoaderListener를 설정한다. -->
   <context-param>
       <param-name>contextClass</param-name>
       <param-value>
           org.springframework.web.context.support.AnnotationConfigWebApplicationContext
       </param-value>
   </context-param>
 
   <!-- 설정 위치는 반드시 콤마나 공백을 구분자로 사용하는 하나 이상의 정규화된 @Configuration 클래스들로 구성되어야 한다. 정규화된 패키지는 컴포넌트 스캔으로 지정될 수도 있다. -->
   <context-param>
       <param-name>contextConfigLocation</param-name>
       <param-value>com.acme.AppConfig</param-value>
   </context-param>
 
   <!-- ContextLoaderListener를 사용해서 루트 어플리케이션 시작한다. -->
   <listener>
       <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
   </listener>
 
   <!-- Spring MVC DispatcherServlet 시작 -->
   <servlet>
       <servlet-name>dispatcher</servlet-name>
       <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
       <!-- 기본 XmlWebApplicationContext 대신 AnnotationConfigWebApplicationContext를 사용한 DispatcherServlet 설정한다. -->
       <init-param>
           <param-name>contextClass</param-name>
           <param-value>
               org.springframework.web.context.support.AnnotationConfigWebApplicationContext
           </param-value>
       </init-param>
       <!-- 다시한번, 설정 위치는 반드시 콤마나 공백을 구분자로 사용하는 하나 이상의 정규화된 @Configuration 클래스들로 구성되어야 한다. -->
       <init-param>
           <param-name>contextConfigLocation</param-name>
           <param-value>com.acme.web.MvcConfig</param-value>
       </init-param>
   </servlet>
 
   <!-- /app/*에 대한 모든 요청을 디스패쳐 서블릿에 매핑한다. -->
   <servlet-mapping>
       <servlet-name>dispatcher</servlet-name>
       <url-pattern>/app/*</url-pattern>
   </servlet-mapping>
</web-app>

@Bean 어노테이션 사용

@Bean은 메소드 레벨 어노테이션이며 XML <bean/> 요소와 동일하며 <bean/>에서 제공하는 일부 속성을 지원한다. 또한, @Configuration 어노테이션 또는 @Component 어노테이션 클래스에서 @Bean 어노테이션을 사용할 수 있다.

Bean 선언

메소드에 @Bean 어노테이션을 사용하면 Bean으로 선언된다. 이 메서드를 사용하여 메서드의 반환 값으로 지정된 유형의 ApplicationContext 내에 Bean 정의를 등록되며 Bean 이름은 메서드 이름과 동일하다.
다음 예제에서 @Bean 메소드 선언을 확인할 수 있다.

@Configuration
public class AppConfig {
   @Bean
   public TransferServiceImpl transferService() {
       return new TransferServiceImpl();
   }
}

위 예제는 다음 Spring XML과 정확히 동일하다.

<beans>
   <bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>

Bean 종속성

@Bean 어노테이션 메서드는 해당 Bean을 빌드하는 데 필요한 종속성을 설명하는 임의 개수의 매개변수를 가질 수 있다.
예를 들어 TransferService에 AccountRepository가 필요한 경우 다음 예제와 같이 메서드 매개 변수를 사용하여 해당 종속성을 구체화할 수 있다.

@Configuration
public class AppConfig {
   @Bean
   public TransferService transferService(AccountRepository accountRepository) {
       return new TransferServiceImpl(accountRepository);
   }
}

@Scope 어노테이션 사용

@Bean 어노테이션으로 정의된 Bean이 특정 범위를 갖도록 지정할 수 있으며, 선언된 Bean의 범위는 Bean scope에 지정된 표준 범위를 사용할 수 있다. 기본 범위는 싱글톤이지만 다음 예제와 같이 @Scope 어노테이션으로 이를 재정의할 수 있다.

@Configuration
public class MyConfiguration {
   @Bean
   @Scope("prototype")
   public Encryptor encryptor() {
       // ...
   }
}

@Configuration 어노테이션 사용

@Configuration은 객체가 Bean 정의의 소스임을 나타내는 클래스 수준 어노테이션이며 @Configuration 클래스는 @Bean 어노테이션 메서드를 통해 빈을 선언한다.
@Configuration 클래스의 @Bean 메소드에 대한 호출은 bean 간 종속성을 정의하는 데에도 사용할 수 있다.

@Configuration
public class AppConfig {
   @Bean
   public BeanOne beanOne() {
       return new BeanOne(beanTwo());
   }
 
   @Bean
   public BeanTwo beanTwo() {
       return new BeanTwo();
   }
}

Java 기반 설정 구성

Spring의 Java 기반 설정 기능을 사용하면 구성의 복잡성을 줄일 수 있는 어노테이션을 작성할 수 있다.

@Import 어노테이션 사용

스프링 XML 파일에서 설정 모듈화에 <import/>요소를 사용하기는 하지만 @Import 어노테이션은 다른 설정 클래스에서 @Bean 설정을 로딩한다.

@Configuration
public class ConfigA {
   @Bean
   public A a() {
       return new A();
   }
}
 
@Configuration
@Import(ConfigA.class)
public class ConfigB {
   @Bean
   public B b() {
       return new B();
   }
}

이제 컨텍스트를 인스턴스화 할 때 ConfigA.class와 ConfigB.class를 둘 다 지정해야하는 대신 ConfigB만 명시적으로 제공하면 된다.

public static void main(String[] args) {
   ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);

   // now both beans A and B will be available...
   A a = ctx.getBean(A.class);
   B b = ctx.getBean(B.class);
}

이 접근방식은 설정을 구성하는 동안 잠재적으로 많은 수의 @Configuration 클래스들을 기억해야 하는 대신 하나의 클래스만 다루면 되므로 컨테이너 인스턴스화를 단순화한다.

임포트한 @Bean 정의에 의존성 주입

위의 예제는 작동하지만 단순하다. 대부분의 실제 시나리오에서 Bean은 다른 설정 클래스들에 대한 의존성을 가지게 된다.
XML을 사용할 때 컴파일러가 관여하지 않고 그냥 ref=“someBean”만 선언한 뒤 스프링이 컨테이너를 인스턴스화 하면 제대로 동작하기를 믿으면 되기 때문에 의존성 자체는 이슈가 아니었다.
@Configuration를 사용할 때 자바 컴파일러는 다른 빈에 대한 참조는 유효한 자바문법이어야 한다는 제약을 설정 모델에 둔다.

다행히도 이 문제를 해결하는 것은 간단하다. 이미 논의한 것처럼 @Bean 메소드는 Bean 종속성을 설명하는 임의의 수의 매개변수를 가질 수 있다.
각각 다른 클래스에서 선언된 Bean에 따라 여러 @Configuration 클래스가 있는 다음과 같이 구성할 수 있다.

@Configuration
public class ServiceConfig {
   @Bean
   public TransferService transferService(AccountRepository accountRepository) {
       return new TransferServiceImpl(accountRepository);
   }
}

@Configuration
public class RepositoryConfig {
   @Bean
   public AccountRepository accountRepository(DataSource dataSource) {
       return new JdbcAccountRepository(dataSource);
   }
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
   @Bean
   public DataSource dataSource() {
       // return new DataSource
   }
}

public static void main(String[] args) {
   ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
   // everything wires up across configuration classes...
   TransferService transferService = ctx.getBean(TransferService.class);
   transferService.transfer(100.00, "A123", "C456");
}

참고자료

1.14 - Environment Abstraction

Environment Abstraction은 Spring에서 프로파일과 속성을 관리하기 위한 Environment 인터페이스를 통해 애플리케이션 환경을 추상화하고 제어하는 기능이다.

Environment Abstraction

개요

Environment Abstraction은 환경에 대한 추상화로 Spring에서 제공하는 Environment 인터페이스를 이용한다.
Environment 인터페이스는 애플리케이션 환경의 두 가지 주요 측면을 모델링하는 컨테이너에 통합된 추상화로, profiles 나 properties처럼 프로그램의 환경 변수나 Application의 프로필을 관리할 때 사용하게 된다.

Profile은 지정된 프로파일이 활성화된 경우에만 컨테이너에 등록되는 명명된 빈 정의의 논리적 그룹이다.
Bean은 XML 또는 주석으로 정의된 프로필에 할당될 수 있다. 프로필과 관련된 환경 개체의 역할은 현재 활성화된 Profile(있는 경우)과 기본적으로 활성화되어야 하는 Profile(있는 경우)을 결정하는 것이다.

Properties는 거의 모든 애플리케이션에서 중요한 역할을 하며 특성 파일, JVM 시스템 특성, 시스템 환경 변수, JNDI, 서블릿 컨텍스트 매개변수, 임시 특성 오브젝트, 맵 오브젝트 등 다양한 소스에서 생성될 수 있다.
속성과 관련된 환경 개체의 역할은 사용자에게 속성 소스를 구성하고 속성을 해결하기 위한 편리한 서비스 인터페이스를 제공하는 것이다.

설명

Bean 정의 Profile

Bean 정의 프로파일은 아래와 같은 상황에서 코어 컨테이너에서 서로 다른 환경에서 서로 다른 Bean을 등록할 수 있는 메커니즘을 제공한다.

  • QA 또는 생산 시 JNDI에서 동일한 데이터 소스를 조회하는 것과 개발 중인 메모리 내 데이터 소스에 대해 작업
  • 애플리케이션을 성능 환경에 배포할 때만 모니터링 인프라를 등록
  • 고객 A 대 고객 B 배치를 위한 Bean의 사용자 정의된 구현을 등록

@Profile 사용

@Profile 어노테이션을 사용하면 하나 이상의 지정된 프로필이 활성 상태일 때 구성 요소가 등록에 적합함을 나타낼 수 있다. @Profile를 사용하여 아래와 같이 데이터소스를 구성할 수 있다.

// standaloneDataSource는 development profile 에서만 사용
@Configuration
@Profile("development")
public class StandaloneDataConfig {
   @Bean
   public DataSource dataSource() {
       return new EmbeddedDatabaseBuilder()
           .setType(EmbeddedDatabaseType.HSQL)
           .addScript("classpath:com/bank/config/sql/schema.sql")
           .addScript("classpath:com/bank/config/sql/test-data.sql")
           .build();
   }
}

// jndiDataSource는 production profile 에서만 사용
@Configuration
@Profile("production")
public class JndiDataConfig {
   @Bean(destroyMethod="")
   public DataSource dataSource() throws Exception {
       Context ctx = new InitialContext();
       return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
   }
}

XML Profile 설정

XML <bean/> 에서 제공하는 profile 요소를 이용하여 Profile을 구성할 수 있다. 위의 데이터소스 예제를 아래와 같이 변경할 수 있다.

<beans profile="development"
   xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:jdbc="http://www.springframework.org/schema/jdbc"
   xsi:schemaLocation="...">
   <jdbc:embedded-database id="dataSource">
       <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
       <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
   </jdbc:embedded-database>
</beans>
 
<beans profile="production"
   xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xmlns:jee="http://www.springframework.org/schema/jee"
   xsi:schemaLocation="...">
   <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>

그리고, 아래와 같이 어떤 Profile을 활성화할지 설정할 수 있다.

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

@PropertySource 사용

@PropertySource 어노테이션은 PropertySource를 Spring의 환경에 추가하기 위한 편리하고 선언적인 메커니즘을 제공한다. testbean.name=myTestBean(키-값)을 포함하는 app.properties라는 파일이 주어지면 @Configuration 클래스는 testBean.getName()에 대한 호출이 myTestBean을 반환하는 방식으로 @PropertySource를 사용한다.

@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {

   @Autowired
   Environment env;

   @Bean
   public TestBean testBean() {
       TestBean testBean = new TestBean();
       testBean.setName(env.getProperty("testbean.name"));
       return testBean;
   }
}

이미 등록된 속성(예: 시스템 속성 또는 환경 변수)이 있는 경우 기본값과 함께 ${…} 자리 표시자에 기술할 수 있다. 기본값이 지정되지 않고 속성을 확인할 수 없는 경우는 IllegalArgumentException이 발생한다. 아래 예제는 my.placeholder 속성이 있는 경우 my.placeholder 속성값을 사용하고 없을 경우 기본값인 default/path를 사용하겠다고 기술한 것이다.

@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {

   @Autowired
   Environment env;

   @Bean
   public TestBean testBean() {
       TestBean testBean = new TestBean();
       testBean.setName(env.getProperty("testbean.name"));
       return testBean;
   }
}

참고자료

1.15 - Inversion of Control

이 문서는 Martin Fowler가 작성한 “Inversion of Control” 글을 번역하고 일부 의역한 내용이다.

Inversion of Control

개요

본 문서는 Martin Fowler가 저술한 Inversion of Control 문서를 번역 및 일부 의역한 것이다.

설명

Inversion of Control(IoC)는 당신이 프레임워크를 확장할 때 마주치게 되는 일반적인 사상이다. 또한, 프레임워크를 정의하는 특징이기도 하다.

간단한 예제를 생각해보자. 명령줄의 질문을 통해 사용자로부터 어떠한 정보를 입력받는 프로그램을 작성한다고 생각해보자. 나는 아마 다음과 같은 것을 작성할 것이다.

#ruby
puts 'What is your name?'
name = gets
process_name(name)
puts 'Waht is your quest?'
quest = gets
process_quest(quest)

위 예제에서, 내가 작성한 코드는 제어권을 가지고 있다 : 질문을 언제 할 것인지, 대답은 언제 읽을 것인지, 그리고 그런 결과들을 언제 처리할 것인지 등을 결정하고 있다.

만약 같은 일을 하기 위해서 윈도우 시스템을 사용한다면, 나는 다음과 같이 윈도우를 설정할 것이다.

require 'tk'
root = TkRoot.new()
name_label = TkLabel.new() {text "What is Your Name?"}
name_label.pack
name = TkEntry.new(root).pack
name.bind("FocusOut") {process_name(name)}
quest_label = TkLabel.new() {text "What is Your Quest?"}
quest_label.pack
quest = TkEntry.new(root).pack
quest.bind("FocusOut") {process_quest(quest)}
Tk.mainloop()

위 두 프로그램은 제어의 흐름에 있어서 큰 차이점을 가지고 있다. 특히 process_name과 process_quest 메소드가 호출되는 시점에 대한 제어가 다르다. 명령줄 방식을 사용한 프로그램의 경우, 나는 메소드들이 호출되는 시점을 직접 제어했다. 하지만, 윈도우를 사용한 프로그램은 그러지 않았다. 대신 나는 윈도우 시스템에게 제어권을 넘겨주었다(Tk.mainloop 명령어를 사용하여). 윈도우 시스템은 내가 폼을 생성할 때 만든 결합 정보를 이용하여 내 메소드들을 호출할 시점을 결정한다. 즉, 제어가 역전된 것이다 - 내가 프레임워크를 호출하는 것이 아니라 프레임워크가 나를 호출하는 것이다. 이 사상이 Inversion of Control이다(또한 Hollywood Principle - “Dont’ call us, we’ss call you”라고도 한다).

프레임워크의 가장 중요한 특징은, 사용자가 프레임워크를 사용하기 위해 만든 메소드들이 사용자 어플리케이션 코드에서 호출되는 것보다 프레임워크에 의해 호출되는 것이 더 종종 일어난다는 것이다. 프레임워크는 어플리케이션의 활동을 조합하고 순차적으로 수행하는 메인 프로그램의 역할을 수행한다. 이러한 제어의 역전이 프레임워크가 확장가능한 뼈대로서의 기능을 수행할 수 있도록 해준다. 사용자는 프레임워크가 정의한 일반적인 알고리즘을 확장하여 특정 어플리케이션을 위한 메소드를 생성한다.

Inversion of Control은 프레임워크를 라이브러리와 구별짓게 만드는 핵심이다. 라이브러리는 본질적으로 당신이 호출할 수 있는 기능들의 집합이다(요즘은 이러한 기능들이 클래스를 구성하고 있다). 한번 호출되면 작업을 수행하고 클라이언트에게 다시 제어권을 넘긴다.

프레임워크는 일부 추상적인 설계를 가지고 있으며, 미리 정의된 행동 방식을 가지고 있다. 프레임워크를 사용하기 위해서 당신은 프레임워크가 제공하는 클래스를 상속하거나 또는 작성한 클래스를 프레임워크에 삽입함으로써 프레임워크에 존재하는 확장 지점에 당신이 원하는 행동 방식을 삽입해야 한다. 그러면 프레임워크는 각각의 확장 지점에서 당신의 코드를 호출할 것이다.

당신이 만든 코드를 삽입하는 방식에는 여러가지가 있다. 위 ruby 예제의 경우, 우리는 이벤트 이름과 Closure를 변수로 갖는 text entry field의 bind 메소드를 호출했다. Text entry box는 이벤트를 감지할 때 마다 closure의 코드를 호출한다. 이처럼 closure를 이용하는 것은 매우 간편하지만, 이를 지원하는 언어는 많지 않다.

또다른 방법으로는 프레임워크가 이벤트를 정의하고, 클라이언트가 이들 이벤트를 받는 방법이 있다 .NET 플랫폼이 이벤트를 선언할 수 있는 언어적 특징을 가진 좋은 예이다. 당신은 delegate를 사용하여 메소드와 이벤트를 연결할 수 있다.

위 방식(실제로는 둘은 같다)은 단순한 경우에는 잘 동작한다. 하지만 때때로 당신은 하나의 확장 지점에서 여러개의 메소드를 접합하기를 원할 수도 있다. 이런 경우 프레임워크는 인터페이스를 정의하고 클라이언트가 이를 구현할 수 있다.

EJB가 이런 inversion of control 형식의 좋은 예이다. 당신이 session bean을 개발할 때, 당신은 여러 생명주기 지점에서 EJB 컨테이너에 의해 호출되는 다양한 메소드를 구현할 수 있다. 예를 들어, Session Bean 인터페이스는 ejbRemote, ejbPassivate(2차 저장소에 저장됨), 그리고 ejbActivate(비활성 상태에서 복원됨)를 정의한다. 당신은 이 메소드들이 호출되는 시점에 대한 제어권을 가지지 않고, 다만 무엇을 할 것인지만 결정한다. 컨테이너가 우리를 호출하고, 우리는 그러지 않는다.

Inversion of Control의 복잡한 경우가 존재하지만, 당신은 좀더 단순한 상황에서 효과를 볼 수 있다. Template method가 좋은 예이다: 부모클래스는 제어의 흐름을 정의하고, 자식클래스는 메소드를 재정의하거나 추상메소드를 구현함으로써 확장할 수 있다. JUnit에서, 프레임워크는 당신이 테스트 기반을 생성하고 삭제하기 위해 작성한 setUp과 tearDown 메소드를 호출한다. 프레임워크가 호출하면, 당신의 코드는 반응한다. 즉, 제어가 역전된 것이다.

요즘 IoC 컨테이너의 등장에 따라 inversion of control의 의미에 대한 일부 혼동이 발생하고 있다. 몇몇 사람들은 이 문서에서 설명한 일반적인 원리와 이들 컨테이너에서 사용하고 있는 inversion of control의 특수한 형식인 dependency injection을 혼동하고 있다. 이름에서 약간 혼란이 야기된다고 할 수 있는데,(또한 모순적이기도 하다) IoC 컨테이너는 일반적으로 EJB의 경쟁상대로 간주되지만 EJB가 inversion of control을 더 많이 사용하기 때문이다.

어원: 내가 알기론, Inversion of Control이란 단어는 1988년 Object-Oriented Programming 저널에 발표된 Johnson and Foote의 논문 Designing Reusable Classes에서 처음 사용되었다. 이 논문은 잘 작성된 눈문 중의 하나로, 발표 이후 15년에 이른 현재까지도 읽을만한 가치가 있다. 그들은 어딘가 다른 곳에서 단어를 가져왔다고 하지만, 어디였는지는 기억하지 못한다. 이 단어는 object-oriented 커뮤니티 속으로 점점 더 녹아들었고 결국 책 Gang of Four에도 나타나게 되었다. 좀 더 화려한 별칭인 ‘Hollywood Principle’은 1983년 Mesa에 실린 Richard Sweet의 논문에서 고안된 걸로 보인다. 설계 목표에서 그는 다음과 같이 기술하고 있다. “Don’t call us, we’ll call you (Hollywood’s Law): A tool should arrange for Tajo to notify it when the user wishes to communicate some event to the tool, rather than adopt an’ ask the user for a command and execute it’ model.” John Vlissides는 column for C++ report에서 ‘Hollywood Principle’ 의 개념을 잘 설명하고 있다. (어원에 대해서 도움을 준 Brian Foote과 Ralph Johnson에게 감사드린다.)

참고자료

1.16 - Generic

Spring 4 이상에서는 Autowired 및 Qualifier를 보완하여 제네릭(Generic) 타입의 의존성 주입을 지원한다.

Generic

개요

Spring4 Generic은 Autowired 및 Qualifired를 보완하여 Generic을 지원합니다.

설명

기존 Autowire 및 Qualifier의 기능에 대하여 확장하여 Spring4에서 추가로 지원하는 Generic 타입의 Autowire기능에 대하여 알아본다.

Autowire 및 Qualifier

Autowiring 예제

다음은 Customer 클래스에 Person property로 Autowire하는 예제이다.

package com.egovframe.common;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
 
public class Customer {
   @Autowired
   private Person person;
   //...
}

Autowire의 문제점

다음과 같은경우 Person에 Autowire로 주입될수 없다.

<beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
   	http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
 
   <bean class ="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/>
 
   <bean id="customer" class="com.egovframe.common.Customer" />
 
   <bean id="personA" class="com.egovframe.common.Person" >
   	<property name="name" value="GentlemanA" />
   </bean>
 
   <bean id="personB" class="com.egovframe.common.Person" >
   	<property name="name" value="GentlemanB" />
   </bean>
 
</beans>

같은 타입이 2개이므로 다음과 같은 오류를 발생시킨다.

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: 
   No unique bean of type [com.egovframe.common.Person] is defined: 
   	expected single matching bean but found 2: [personA, personB]

Qualifier 사용

Qualifier 어노테이션에 personA를 특정하여 처리 할수 있으나 개별적으로 하나하나 지정해야 하는 문제가 있다.

package com.egovframe.common;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
 
public class Customer {
   @Autowired
   @Qualifier("personA")
   private Person person;
   //...
}

Spring 4 Generics

Generic 설정

추상화 클래스 Employee를 구현한 Manager,Admin을 Generic으로 설정한다. 이경우 Spring 4의 Generic에 대하여 Autowire 주입 대상이 된다.

package generic;
 
import org.springframework.beans.factory.annotation.Autowired;
 
public class InjectBeans {
   @Autowired
   Employee<Manager> emp1;
 
   @Autowired
   Employee<Admin> emp2;
 
   public void invokeManager(){
       emp1.empSection();
   }
 
   public void invokeAdmin(){
   	emp2.empSection();
   }
}

주입대상 구현 클래스

Employee는 추상클래스이며 상속을 통하여 구현된다. Admin과 Manager는 구현클래스이며 Autowire 어노테이션에의해 자동 주입된다.

package generic;

public abstract class Employee<T> {
   public void printCompanyName(){
       System.out.println("Display : Company Name");
   }

   public abstract void empSection();
}
package generic;

public class Admin extends Employee<Admin>{
   public void empSection(){
       System.out.println("Display : Admin Section");
   }
}
package generic;

public class Manager extends Employee<Manager>{
   public void empSection(){
       System.out.println("Display : Manager Section");
   }
}

수행결과

구현 클래스 각각의 empSection()이 수행되어 다음과 같은 결과가 출력된다.

6월 23, 2015 11:23:54 오전 org.springframework.context.annotation.AnnotationConfigApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@1262a85: startup date [Tue Jun 23 11:23:54 KST 2015]; root of context hierarchy
Display : Admin Section
Display : Manager Section

Spring 3에서의 complie 및 실행

Spring 3에서는 complie은 문제가 없으나 runtime시에 Generic 타입에 대한 주입기능이 없으므로 다음과 같은 오류가 발생한다.

6월 23, 2015 10:38:25 오전 org.springframework.context.annotation.AnnotationConfigApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@1110f31: startup date [Tue Jun 23 10:38:25 KST 2015]; root of context hierarchy
6월 23, 2015 10:38:26 오전 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@17f70bf: defining beans [org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,springMVCConfiguration,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor,getManager,InjectBeans,getAdmin]; root of factory hierarchy
6월 23, 2015 10:38:26 오전 org.springframework.beans.factory.support.DefaultListableBeanFactory destroySingletons
INFO: Destroying singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@17f70bf: defining beans [org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,springMVCConfiguration,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor,getManager,InjectBeans,getAdmin]; root of factory hierarchy
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'InjectBeans': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: generic.Employee generic.InjectBeans.emp1; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [generic.Employee] is defined: expected single matching bean but found 2: getManager,getAdmin
   at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:289)
   at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1146)
   at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:519)
   at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:458)
   at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:296)
   at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
   at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:293)
   at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
   at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:633)
   at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:932)
   at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:479)
   at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:73)
   at generic.SpringMVCApplicationDemo.main(SpringMVCApplicationDemo.java:15)
Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field: generic.Employee generic.InjectBeans.emp1; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [generic.Employee] is defined: expected single matching bean but found 2: getManager,getAdmin
   at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:517)
   at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:87)
   at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:286)
   ... 12 more
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [generic.Employee] is defined: expected single matching bean but found 2: getManager,getAdmin
   at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:870)
   at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:775)
   at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:489)
   ... 14 more

1.17 - AOP 서비스

AOP 서비스는 관점지향 프로그래밍(AOP)을 구현하며, 실행환경에서는 Spring AOP를 사용한다. 이 장에서는 AOP의 개요와 Spring의 AOP 지원에 대해 설명한다.

AOP 서비스

개요

AOP 서비스는 관점지향 프로그래밍(Aspect Oriented Programming: AOP) 사상을 구현하고 지원한다. 실행환경 AOP 서비스는 Spring AOP를 사용한다. 본 장에서는 AOP의 개요 및 Spring의 AOP 지원을 중심으로 살펴본다.

설명

AOP 개요

개별 프로그래밍 언어는 프로그램 개발을 위해 고유한 관심사 분리(Separation of Concerns) 패러다임을 갖는다. 예를 들면 절차적 프로그래밍은 상태값을 갖지 않는 연속된 함수들의 실행을 프로그램으로 이해하고 모듈을 주요 분리 단위로 정의한다. 객체지향 프로그래밍은 일련의 함수 실행이 아닌 상호작용하는 객체들의 집합으로 보며 클래스를 주요 단위로 한다.
객체지향 프로그래밍은 많은 장점에도 불구하고, 다수의 객체들에 분산되어 중복적으로 존재하는 공통 관심사가 존재한다. 이들은 프로그램을 복잡하게 만들고, 코드의 변경을 어렵게 한다.
관점 지향 프로그래밍(AOP, Aspect-Oriented Programming)은 이러한 객체지향 프로그래밍의 문제점을 보완하는 방법으로 핵심 관심사를 분리하여 프로그램 모듈화를 향상시키는 프로그래밍 스타일이다. AOP는 객체를 핵심 관심사와 횡단 관심사로 분리하고, 횡단 관심사를 관점(Aspect)이라는 모듈로 정의하고 핵심 관심사와 엮어서 처리할 수 있는 방법을 제공한다.

  • 관점(Aspect)은 프로그램의 핵심 관심사에 걸쳐 적용되는 공통 프로그램 영역을 의미한다. 예를 들면 로깅, 인증, 권한확인, 트랜잭션은 비지니스 기능 구현시에 공통적으로 적용되는 요소이며 하나의 관점으로 정의될 수 있다.
  • 핵심 관심사(Core concern)는 프로그램을 작성하려는 핵심 가치와 목적이 드러난 관심 영역으로 보통 핵심 비지니스 기능에 해당한다.
  • 횡단 관심사(Cross-cutting concern)는 핵심 관심에 영향을 주는 프로그램의 영역으로, 로깅과 트랜잭션, 인증처리와 같은 시스템 공통 처리 영역이 해당된다.

다음 그림은 객체지향 프로그래밍 개발에서 핵심 관심사와 횡단 관심사가 하나의 코드로 통합되어 개발된 사례를 보여준다.

image

객체지향 프로그래밍 코드에 AOP를 적용하면 다음 그림처럼 각 코드에 분산되어 있던 횡단 관심사는 관점으로 분리되어 정의된다. AOP는 엮기(Weaving)라는 방식을 이용하여 분리된 관점을 핵심 관심사와 엮는다.

image

AOP 주요 개념

관점 지향 프로그래밍은 횡단 관심사를 분리하고 핵심 관심사와 엮어 사용할 수 있는 방법을 제공하며 다음의 몇 가지 새로운 개념을 포함한다.

관점(Aspect)

관점은 구현하고자 하는 횡단 관심사의 기능이다.

결합점(Join point)

결합점은 관점(Aspect)를 삽입하여 실행 가능한 어플리케이션의 특정 지점을 말한다.

포인트컷(Pointcut)

포인트컷은 결합점 집합을 의미한다. 포인트컷은 어떤 결합점을 사용할 것이지를 결정하기 위해 패턴 매칭을 이용하여 룰을 정의한다. 다음 그림은 Spring 2.5에 포함된 bean() 포인트컷을 이용하여 종적 및 횡적으로 빈을 선택하는 예제를 보여준다.

image

충고(Advice)

충고(Advice)는 관점(Aspect)의 실제 구현체로 결합점에 삽입되어 동작할 수 있는 코드이다. 충고는 결합점과 결합하여 동작하는 시점에 따라 before advice, after advice, around advice 타입으로 구분된다.

  • Before advice: joinpoint 전에 수행되는 advice
  • After returning advice: joinpoint가 성공적으로 리턴된 후에 동작하는 advice
  • After throwing advice: 예외가 발생하여 joinpoint가 빠져나갈때 수행되는 advice
  • After (finally) advice: join point를 빠져나가는(정상적이거나 예외적인 반환) 방법에 상관없이 수행되는 advice
  • Around advice: joinpoint 전, 후에 수행되는 advice

엮기(Weaving)

엮기는 관점(Aspect)을 대상 객체에 적용하여 새로운 프록시 객체를 생성하는 과정이다. 엮기 방식은 다음과 같이 구분된다.

  • 컴파일 시 엮기: 별도 컴파일러를 통해 핵심 관심사 모듈의 사이 사이에 관점(Aspect) 형태로 만들어진 횡단 관심사 코드들이 삽입되어 관점(Aspect)이 적용된 최종 바이너리가 만들어지는 방식이다. (ex. AspectJ, …)
  • 클래스 로딩 시 엮기: 별도의 Agent를 이용하여 JVM이 클래스를 로딩할 때 해당 클래스의 바이너리 정보를 변경한다. 즉, Agent가 횡단 관심사 코드가 삽입된 바이너리 코드를 제공함으로써 AOP를 지원하게 된다. (ex. AspectWerkz, …)
  • 런타임 엮기: 소스 코드나 바이너리 파일의 변경없이 프록시를 이용하여 AOP를 지원하는 방식이다. 프록시를 통해 핵심 관심사를 구현한 객체에 접근하게 되는데, 프록시는 핵심 관심사 실행 전후에 횡단 관심사를 실행한다. 따라서 프록시 기반의 런타임 엮기의 경우 메소드 호출시에만 AOP를 적용할 수 있다는 제한점이 있다. (ex. Spring AOP, …)

도입(Introduction)

도입(Introduction)은 새로운 메소드나 속성을 추가한다. Spring AOP는 충고(Advice)를 받는 대상 객체에 새로운 인터페이스를 추가할 수 있다.

AOP 프록시(Proxy)

AOP 프록시(Proxy)는 대상 객체(Target Object)에 Advice가 적용된 후 생성되는 객체이다.

대상 객체(Target Object)

대상 객체는 충고(Advice)를 받는 객체이다. Spring AOP는 런타임 프록시를 사용하므로 대상 객체는 항상 프록시 객체가 된다.

Spring의 AOP 지원

스프링은 프록시 기반의 런타임 Weaving 방식을 지원한다. 스프링은 AOP 구현을 위해 다음 세가지 방식을 제공한다. 이 중 @AspectJ 어노테이션과 XML 스키마를 이용한 AOP 방식을 상세히 살펴본다.

실행환경 AOP 가이드라인

* 실행환경 AOP 가이드라인

참고자료

1.18 - @AspectJ 어노테이션을 이용한 AOP 지원

@AspectJ는 Java 5 어노테이션을 사용한 일반 Java 클래스로 관점(Aspect)를 정의하는 방식이다. @AspectJ 방식은 AspectJ 5 버전에서 소개되었으며, Spring은 2.0 버전부터 AspectJ 5 어노테이션을 지원한다. Spring AOP 실행환경은 AspectJ 컴파일러나 직조기(Weaver)에 대한 의존성이 없이 @AspectJ 어노테이션을 지원한다.

@AspectJ 어노테이션을 이용한 AOP 지원

개요

@AspectJ는 Java 5 어노테이션을 사용한 일반 Java 클래스로 관점(Aspect)를 정의하는 방식이다. @AspectJ 방식은 AspectJ 5 버전에서 소개되었으며, Spring은 2.0 버전부터 AspectJ 5 어노테이션을 지원한다. Spring AOP 실행환경은 AspectJ 컴파일러나 직조기(Weaver)에 대한 의존성이 없이 @AspectJ 어노테이션을 지원한다.

설명

@AspectJ 설정하기

@AspectJ를 사용하기 위해서 다음 코드를 Spring 설정에 추가한다.

<aop:aspectj-autoproxy/>

관점(Aspect) 정의하기

클래스에 @Aspect 어노테이션을 추가하여 Aspect를 생성한다. @Aspect 설정이 되어 있는 경우 Spring은 자동적으로 @Aspect 어노테이션을 포함한 클래스를 검색하여 Spring AOP 설정에 반영한다.

import org.aspectj.lang.annotation.Aspect;
 
@Aspect
public class AspectUsingAnnotation {
 ..
}

포인트컷(Pointcut) 정의하기

포인트컷은 결합점(Join points)을 지정하여 충고(Advice)가 언제 실행될지를 지정하는데 사용된다. Spring AOP는 Spring 빈에 대한 메소드 실행 결합점만을 지원하므로, Spring에서 포인트컷은 빈의 메소드 실행점을 지정하는 것으로 생각할 수 있다. 다음 예제는 egovframework.rte.fdl.aop.sample 패키지 하위의 Sample 명으로 끝나는 클래스의 모든 메소드 수행과 일치할 ’targetMethod’ 라는 이름의 pointcut을 정의한다.

@Aspect
public class AspectUsingAnnotation {
   ...
   @Pointcut("execution(public * org.egovframe.rte.fdl.aop.sample.*Sample.*(..))")
   public void targetMethod() {
       // pointcut annotation 값을 참조하기 위한 dummy method
   }
   ...
}

포인트컷 지정자(Designators)

Spring에서 포인트컷 표현식에 사용될 수 있는 지정자는 다음과 같다. 포인트컷은 모두 public 메소드를 대상으로 한다.

  • execution: 메소드 실행 결합점(join points)과 일치시키는데 사용된다.
  • within: 특정 타입에 속하는 결합점을 정의한다.
  • this: 빈 참조가 주어진 타입의 인스턴스를 갖는 결합점을 정의한다.
  • target: 대상 객체가 주어진 타입을 갖는 결합점을 정의한다.
  • args: 인자가 주어진 타입의 인스턴스인 결합점을 정의한다.
  • @target: 수행중인 객체의 클래스가 주어진 타입의 어노테이션을 갖는 결합점을 정의한다.
  • @args: 전달된 인자의 런타임 시의 타입이 주어진 타입의 어노테이션을 갖는 결합점을 정의한다.
  • @within: 주어진 어노테이션을 갖는 타입 내 결합점을 정의한다.
  • @annotation: 결합점의 대상 객체가 주어진 어노테이션을 갖는 결합점을 정의한다.

포인트컷 표현식 조합하기

포인트컷 표현식은 ‘&&’, ‘||’ 그리고 ‘!’ 를 사용하여 조합할 수 있다.

@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
 
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}
 
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}

포인트컷 정의 예제

Spring AOP에서 자주 사용되는 포인트컷 표현식의 예를 살펴본다.

Pointcut선택된 Joinpoints
execution(public * *(..))public 메소드 실행
execution(* set*(..))이름이 set으로 시작하는 모든 메소드명 실행
execution(* set*(..))이름이 set으로 시작하는 모든 메소드명 실행
execution(* com.xyz.service.AccountService.*(..))AccountService 인터페이스의 모든 메소드 실행
execution(* com.xyz.service.*.*(..))service 패키지의 모든 메소드 실행
execution(* com.xyz.service..*.*(..))service 패키지와 하위 패키지의 모든 메소드 실행
within(com.xyz.service.*)service 패키지 내의 모든 결합점
within(com.xyz.service..*)service 패키지 및 하위 패키지의 모든 결합점
this(com.xyz.service.AccountService)AccountService 인터페이스를 구현하는 프록시 개체의 모든 결합점
target(com.xyz.service.AccountService)AccountService 인터페이스를 구현하는 대상 객체의 모든 결합점
args(java.io.Serializable)하나의 파라미터를 갖고 전달된 인자가 Serializable인 모든 결합점
@target(org.springframework.transaction.annotation.Transactional)대상 객체가 @Transactional 어노테이션을 갖는 모든 결합점
@within(org.springframework.transaction.annotation.Transactional)대상 객체의 선언 타입이 @Transactional 어노테이션을 갖는 모든 결합점
@annotation(org.springframework.transaction.annotation.Transactional)실행 메소드가 @Transactional 어노테이션을 갖는 모든 결합점
@args(com.xyz.security.Classified)단일 파라미터를 받고, 전달된 인자 타입이 @Classified 어노테이션을 갖는 모든 결합점
bean(accountRepository)“accountRepository” 빈
!bean(accountRepository)“accountRepository” 빈을 제외한 모든 빈
bean(*)모든 빈
bean(account*)이름이 ‘account’로 시작되는 모든 빈
bean(*Repository)이름이 “Repository”로 끝나는 모든 빈
bean(accounting/*)이름이 “accounting/“로 시작하는 모든 빈
bean(*dataSource)

충고(Advice) 정의하기

충고(Advice)는 관점(Aspect)의 실제 구현체로 포인트컷 표현식과 일치하는 결합점에 삽입되어 동작할 수 있는 코드이다. 충고는 결합점과 결합하여 동작하는 시점에 따라 before advice, after advice, around advice 타입으로 구분된다.

Before advice

Before advice는 @Before 어노테이션을 사용한다. 다음은 Before 충고를 사용하는 예제이다. Before 충고인 beforeTargetMethod() 메소드는 targetMethod()로 정의된 포인트컷 전에 수행된다.

@Aspect
public class AspectUsingAnnotation {
   ..
   @Before("targetMethod()")
   public void beforeTargetMethod(JoinPoint thisJoinPoint) {
       Class clazz = thisJoinPoint.getTarget().getClass();
       String className = thisJoinPoint.getTarget().getClass().getSimpleName();
       String methodName = thisJoinPoint.getSignature().getName();
       System.out.println("AspectUsingAnnotation.beforeTargetMethod executed.");
       System.out.println(className + "." + methodName + " executed.");
   }
}

After returning advice

After returing 충고는 정상적으로 메소드가 실행될 때 수행된다. After returning 충고는 @AfterReturing 어노테이션을 사용한다. 다음은 After returning 충고를 사용하는 예제이다. afterReturningTargetMethod() 충고는 targetMethod()로 정의된 포인트컷 후에 수행된다. targetMethod() 포인트컷의 실행 결과는 retVal 변수에 저장되어 전달된다.

@Aspect
public class AspectUsingAnnotation {
   ..
   @AfterReturning(pointcut = "targetMethod()", returning = "retVal")
   public void afterReturningTargetMethod(JoinPoint thisJoinPoint, Object retVal) {
       System.out.println("AspectUsingAnnotation.afterReturningTargetMethod executed." + " return value is [" + retVal + "]");
   }
}

After throwing advice

After throwing 충고는 메소드가 수행 중 예외사항을 반환하고 종료하는 경우 수행된다. After throwing 충고는 @AfterThrowing 어노테이션을 사용한다. 다음은 After throwing 충고를 사용하는 예제이다. afterThrowingTargetMethod() 충고는 targetMethod()로 정의된 포인트컷에서 예외가 발생한 후에 수행된다. targetMethod() 포인트컷에서 발생된 예외는 exception 변수에 저장되어 전달된다. 예제에서는 전달 받은 예외를 한번 더 감싸서 사용자가 쉽게 알아 볼 수 있도록 메시지를 설정하여 반환한다.

@Aspect
public class AspectUsingAnnotation {
   ..
   @AfterThrowing(pointcut = "targetMethod()", throwing = "exception")
   public void afterThrowingTargetMethod(JoinPoint thisJoinPoint,
           Exception exception) throws Exception {
       System.out.println("AspectUsingAnnotation.afterThrowingTargetMethod executed.");
       System.out.println("에러가 발생했습니다.", exception);
       throw new BizException("에러가 발생했습니다.", exception);
   }
}

After (finally) advice

After (finally) 충고는 메소드 수행 후 무조건 수행된다. After (finally) 충고는 @After 어노테이션을 사용한다. After 충고는 정상 종료와 예외 발생 경우를 모두 처리해야 하는 경우에 사용된다. 리소스 해제와 같은 작업이 해당된다. 다음은 After (finally) 충고를 사용하는 예제이다. afterTargetMethod() 충고는 targetMethod()로 정의된 포인트컷 이후에 수행된다.

 @Aspect
public class AspectUsingAnnotation {
    ..
    @After("targetMethod()")
    public void afterTargetMethod(JoinPoint thisJoinPoint) {
        System.out.println("AspectUsingAnnotation.afterTargetMethod executed.");
    }
}

Around advice

Around 충고는 메소드 수행 전후에 수행된다. Around 충고는 @Around 어노테이션을 사용한다. 다음은 Around 충고를 사용하는 예제이다. aroundTargetMethod() 충고는 파라미터로 ProceedingJoinPoint을 전달하며 proceed() 메소드 호출을 통해 대상 포인트컷을 실행한다. 포인트컷 수행 결과값인 retVal을 Around 충고 내에서 변환하여 반환할 수 있음을 보여준다.

@Aspect
public class AspectUsingAnnotation {
   ..
   @Around("targetMethod()")
   public Object aroundTargetMethod(ProceedingJoinPoint thisJoinPoint) throws Throwable {
       System.out.println("AspectUsingAnnotation.aroundTargetMethod start.");
       long time1 = System.currentTimeMillis();
       Object retVal = thisJoinPoint.proceed();

       System.out.println("ProceedingJoinPoint executed. return value is [" + retVal + "]");

       retVal = retVal + "(modified)";
       System.out.println("return value modified to [" + retVal + "]");

       long time2 = System.currentTimeMillis();
       System.out.println("AspectUsingAnnotation.aroundTargetMethod end. Time(" + (time2 - time1) + ")");
       return retVal;
   }
}

관점(Aspect) 실행하기

앞서 정의한 관점(Aspect)가 정상적으로 동작하는지 확인하기 위해 테스트 코드를 이용해 확인해 본다. AnnotationAspectTest 클래스는 대상 메소드 수행 시 예외 없이 정상 실행하는 경우와 예외 발생의 경우를 구분해서 테스트한다.

정상 실행의 경우

testAnnotationAspect() 함수는 대상 메소드가 정상 수행되는 사례를 보여준다. egovframework.rte.fdl.aop.sample 패키지에 속하는 AnnotationAdviceSample 클래스의 someMethod() 메소드는 before, after returning, after finally, around 충고(Advice)가 적용된다.

public class AnnotationAspectTest {
   @Resource(name = "annotationAdviceSample")
   AnnotationAdviceSample annotationAdviceSample;

   @Test
   public void testAnnotationAspect() throws Exception {
       SampleVO vo = new SampleVO();
       ..
       String resultStr = annotationAdviceSample.someMethod(vo);

       assertEquals("someMethod executed.(modified)", resultStr);
   }
}

테스트 코드를 수행한 결과 로그는 다음과 같다.

AspectUsingAnnotation.beforeTargetMethod executed.
AspectUsingAnnotation.aroundTargetMethod start.
ProceedingJoinPoint executed. return value is [someMethod executed.]
return value modified to [someMethod executed.(modified)]
AspectUsingAnnotation.aroundTargetMethod end. Time(78)
AspectUsingAnnotation.afterTargetMethod executed.
AspectUsingAnnotation.afterReturningTargetMethod executed. return value is [someMethod executed.(modified)]

콘솔 로그 출력을 보면 충고(Advice)가 적용되는 순서는 다음과 같다.

  • @Before
  • @Around (대상 메소드 수행 전)
  • 대상 메소드
  • @Around (대상 메소드 수행 후)
  • @After(finally)
  • @AfterReturning

주의할 점은 @Around 충고는 대상 메소드의 반환 값(return value)를 변경 가능하지만, After returning 충고는 반환 값을 참조 가능하지만 변경할 수 없다.

예외 발생의 경우

testAnnotationAspectWithException() 함수는 대상 메소드에 오류가 발생한 사례를 보여준다. egovframework.rte.fdl.aop.sample 패키지에 속하는 AnnotationAdviceSample 클래스의 someMethod() 메소드는 before, after throwing, after finally, around 충고(Advice)가 적용된다.

public class AnnotationAspectTest {
   @Resource(name = "annotationAdviceSample")
   AnnotationAdviceSample annotationAdviceSample;

   @Test
   public void testAnnotationAspectWithException() throws Exception {
       SampleVO vo = new SampleVO();
       // exception 을 발생시키도록 플래그 설정
       vo.setForceException(true);
       ..
       try {
           // vo 의 forceException 플래그가 true 이면 - / by zero 상황을 강제로 처리함
           resultStr = annotationAdviceSample.someMethod(vo);
           fail("exception 을 강제로 발생시켜 이 라인이 수행될 수 없습니다.");
       } catch (Exception e) {
           ..
       }
   }
}

테스트 코드를 수행한 결과 로그는 다음과 같다.

AspectUsingAnnotation.beforeTargetMethod executed.
AspectUsingAnnotation.aroundTargetMethod start.
AspectUsingAnnotation.afterTargetMethod executed.
AspectUsingAnnotation.afterThrowingTargetMethod executed.
에러가 발생했습니다.
java.lang.ArithmeticException: / by zero
...

콘솔 로그 출력을 보면 충고(Advice)가 적용되는 순서는 다음과 같다.

  • @Before
  • @Around (대상 메소드 수행 전)
  • 대상 메소드 (ArithmeticException 예외가 발생한다)
  • @After(finally)
  • @AfterThrowing

예외가 발생하더라도 after 로 정의한 충고(Advice)는 수행되는 것을 확인할 수 있다. After Throwing 충고(Advice)는 에러 메시지를 재설정하고 새로운 예외를 생성하여 전달할 수 있다.

참고자료

1.19 - XML 스키마 기반 AOP 지원

Java 5 버전을 사용할 수 없거나 XML 기반 설정을 선호하는 경우, Spring 2.0 이상에서는 XML 스키마 기반 AOP를 사용할 수 있으며, aop 네임스페이스를 제공한다. 이 방식에서도 @AspectJ AOP에서 사용된 포인트컷 표현식과 충고(Advice) 유형을 동일하게 사용할 수 있다.

XML 스키마 기반 AOP 지원

개요

Java 5 버전을 사용할 수 없거나, XML 기반 설정을 선호한다면, Spring 2.0 이상에서 제공하는 XML 스키마 기반의 AOP를 사용할 수 있다. Spring은 관점(Aspect) 정의를 지원하기 위해 “aop” 네임스페이스를 제공한다. @AspectJ를 이용한 AOP 지원에서 사용된 포인트컷 표현식과 충고(Advice) 유형은 XML 스키마 기반 AOP 지원에도 동일하게 제공된다.

설명

관점(Aspect) 정의하기

Spring 어플리케이션 컨텍스트에서 빈으로 정의된 일반 Java 개체는 관점(Aspect)으로 정의될 수 있다. 관점(Aspect)은 <aop:aspect> 요소를 사용하여 정의한다.

<bean id="adviceUsingXML" class="org.egovframe.rte.fdl.aop.sample.AdviceUsingXML" />
<aop:config>
   <aop:aspect ref="adviceUsingXML">
   ...
   </aop:aspect>
</aop:config>

관점(Aspect)로 정의된 aBean은 Spring 빈처럼 설정되고 의존성 주입이 될 수 있다.

포인트컷(Pointcut) 정의하기

포인트컷은 결합점(Join points)을 지정하여 충고(Advice)가 언제 실행될지를 지정하는데 사용된다. Spring AOP는 Spring 빈에 대한 메소드 실행 결합점만을 지원하므로, Spring에서 포인트컷은 빈의 메소드 실행점을 지정하는 것으로 생각할 수 있다. 다음 예제는 egovframework.rte.fdl.aop.sample 패키지 하위의 Sample 명으로 끝나는 클래스의 모든 메소드 수행과 일치할 ’targetMethod’ 라는 이름의 pointcut을 정의한다. 포인트컷은 <aop:config> 요소 내에 정의한다. 포인트컷 표현식은 AspectJ 포인트컷 표현 언어와 동일하게 사용할 수 있다.

<aop:config>
   <aop:pointcut id="targetMethod" expression="execution(* org.egovframe.rte.fdl.aop.sample.*Sample.*(..))" />
</aop:config>

충고(Advice) 정의하기

충고(Advice)는 관점(Aspect)의 실제 구현체로 포인트컷 표현식과 일치하는 결합점에 삽입되어 동작할 수 있는 코드이다. 충고는 결합점과 결합하여 동작하는 시점에 따라 before advice, after advice, around advice 타입으로 구분된다. @AspectJ를 이용한 AOP와 동일하게 5종류의 충고(Advice)를 지원한다.

Before advice

Before advice는 <aop:aspect> 요소 내에서 <aop:before> 요소를 사용하여 정의한다. 다음은 before 충고를 정의하는 XML의 예제이다. before 충고인 beforeTargetMethod() 메소드는 targetMethod()로 정의된 포인트컷 전에 수행된다.

<aop:aspect ref="adviceUsingXML">
   <aop:before pointcut-ref="targetMethod" method="beforeTargetMethod" />
</aop:aspect>

다음은 before 충고를 구현하고 있는 클래스이다. before 충고를 수행하는 beforeTargetXML()메소드는 해당 포인트컷을 가진 클래스명과 메소드 명을 출력한다.

public class AdviceUsingXML {
   ...
   public void beforeTargetMethod(JoinPoint thisJoinPoint) {
       System.out.println("AdviceUsingXML.beforeTargetMethod executed.");

       Class clazz = thisJoinPoint.getTarget().getClass();
       String className = thisJoinPoint.getTarget().getClass().getSimpleName();
       String methodName = thisJoinPoint.getSignature().getName();

       System.out.println(className + "." + methodName + " executed.");
   }
   ...
}

After returning advice

After returning 충고는 정상적으로 메소드가 실행될 때 수행된다. After 충고는 <aop:aspect> 요소 내에서 <aop:after-returning> 요소를 사용하여 정의한다. 다음은 After returning 충고를 사용하는 예제이다. afterReturningTargetMethod() 충고는 targetMethod()로 정의된 포인트컷 후에 수행된다. targetMethod() 포인트컷의 실행 결과는 retVal 변수에 저장되어 전달된다.

<aop:aspect ref="adviceUsingXML">
   <aop:after-returning pointcut-ref="targetMethod" method="afterReturningTargetMethod" returning="retVal" />
</aop:aspect>

다음은 After returning 충고를 구현하고 있는 클래스이다. After returning 충고를 수행하는 afterReturningTargetMethod()메소드는 해당 포인트컷의 반환값을 출력한다.

public class AdviceUsingXML {
   ...
   public void afterReturningTargetMethod(JoinPoint thisJoinPoint, Object retVal) {
       System.out.println("AdviceUsingXML.afterReturningTargetMethod executed." + return value is [" + retVal + "]");
   }
   ...
}

After throwing advice

After throwing 충고는 메소드가 수행 중 예외사항을 반환하고 종료하는 경우 수행된다. After 충고는 <aop:aspect> 요소 내에서 <aop:after-returning> 요소를 사용하여 정의한다. 다음은 After throwing 충고를 사용하는 예제이다. afterThrowingTargetMethod() 충고는 targetMethod()로 정의된 포인트컷에서 예외가 발생한 후에 수행된다. targetMethod() 포인트컷에서 발생된 예외는 exception 변수에 저장되어 전달된다.

<aop:aspect ref="adviceUsingXML">
   <aop:after-throwing pointcut-ref="targetMethod" method="afterThrowingTargetMethod" throwing="exception" />
</aop:aspect>

다음은 After throwing 충고를 구현하고 있는 클래스이다. After throwing 충고를 수행하는 afterReturningTargetMethod()메소드는 전달 받은 예외를 한번 더 감싸서 사용자가 쉽게 알아 볼 수 있도록 메시지를 설정하여 반환한다.

public class AdviceUsingXML {
   ...
   public void afterThrowingTargetMethod(JoinPoint thisJoinPoint, Exception exception) throws Exception {
       System.out.println("AdviceUsingXML.afterThrowingTargetMethod executed.");
       System.err.println("에러가 발생했습니다.", exception);
       throw new BizException("에러가 발생했습니다.", exception);
   }
   ...
}

After (finally) advice

After (finally) 충고는 메소드 수행 후 무조건 수행된다. After 충고는 <aop:aspect> 요소 내에서 <aop:after> 요소를 사용하여 정의한다. After 충고는 다음은 After (finally) 충고를 사용하는 예제이다. afterTargetMethod() 충고는 targetMethod()로 정의된 포인트컷의 정상 종료 및 예외 발생의 경우 모두에 대해 수행된다. 보통은 리소스 해제와 같은 작업을 수행한다.

<aop:aspect ref="adviceUsingXML">
   <aop:after pointcut-ref="targetMethod" method="afterTargetMethod" />
</aop:aspect>

다음은 After (finally) 충고를 구현하고 있는 클래스이다. After (finally) 충고를 수행하는 afterTargetMethod()메소드는 after 충고가 수행됨을 표시하는 메시지를 출력한다.

public class AdviceUsingXML {
   ...
   public void afterTargetMethod(JoinPoint thisJoinPoint) {
       System.out.println("AdviceUsingXML.afterTargetMethod executed.");
   }
   ...
}

Around advice

Around 충고는 메소드 수행 전후에 수행된다. After 충고는 <aop:aspect> 요소 내에서 <aop:around> 요소를 사용하여 정의한다. Around 충고는 정상 종료와 예외 발생 경우를 모두 처리해야 하는 경우에 사용된다. 리소스 해제와 같은 작업이 해당된다.

<aop:aspect ref="adviceUsingXML">
   <aop:around pointcut-ref="targetMethod" method="aroundTargetMethod" />
</aop:aspect>

다음은 Around 충고를 구현하고 있는 클래스이다. aroundTargetMethod() 충고는 파라미터로 ProceedingJoinPoint을 전달하며 proceed() 메소드 호출을 통해 대상 포인트컷을 실행한다. 포인트컷 수행 결과값인 retVal을 Around 충고 내에서 변환하여 반환할 수 있음을 보여준다.

public class AdviceUsingXML {
   ...
   public Object aroundTargetMethod(ProceedingJoinPoint thisJoinPoint) throws Throwable {
       System.out.println("AdviceUsingXML.aroundTargetMethod start.");
       long time1 = System.currentTimeMillis();
       Object retVal = thisJoinPoint.proceed();

       System.out.println("ProceedingJoinPoint executed. return value is [" + retVal + "]");

       retVal = retVal + "(modified)";
       System.out.println("return value modified to [" + retVal + "]");

       long time2 = System.currentTimeMillis();
       System.out.println("AdviceUsingXML.aroundTargetMethod end. Time(" + (time2 - time1) + ")");
       return retVal;
   }
   ...
}

관점(Aspect) 실행하기

앞서 정의한 관점(Aspect)가 정상적으로 동작하는지 확인하기 위해 테스트 코드를 이용해 확인해 본다. AdviceTest 클래스는 대상 메소드 수행 시 예외 없이 정상 실행하는 경우와 예외 발생의 경우를 구분해서 테스트한다.

정상 실행의 경우

testAdvice() 함수는 대상 메소드가 정상 수행되는 사례를 보여준다. egovframework.rte.fdl.aop.sample 패키지에 속하는 AdviceSample 클래스의 someMethod() 메소드는 before, after returning, after finally, around 충고(Advice)가 적용된다.

public class AdviceTest{
   @Resource(name = "adviceSample")
   AdviceSample adviceSample;

   @Test
   public void testAdvice() throws Exception {
       SampleVO vo = new SampleVO();
       ..
       String resultStr = adviceSample.someMethod(vo);

       assertEquals("someMethod executed.(modified)", resultStr);
   }
}

테스트 코드를 수행한 결과 로그는 다음과 같다.

AdviceUsingXML.beforeTargetMethod executed.
AdviceSample.someMethod executed.
AdviceUsingXML.aroundTargetMethod start.
AdviceUsingXML.afterReturningTargetMethod executed. return value is [someMethod executed.]
AdviceUsingXML.afterTargetMethod executed.
ProceedingJoinPoint executed. return value is [someMethod executed.]
return value modified to [someMethod executed.(modified)]
AdviceUsingXML.aroundTargetMethod end. Time(12)

콘솔 로그 출력을 보면 충고(Advice)가 적용되는 순서는 다음과 같다.

  • @Before
  • @Around (대상 메소드 수행 전)
  • 대상 메소드
  • @AfterReturning
  • @After(finally)
  • @Around (대상 메소드 수행 후)

주의할 점은 @Around 충고는 대상 메소드의 반환 값(return value)를 변경 가능하지만, After returning 충고는 반환 값을 참조 가능하지만 변경할 수 없다.

예외 발생의 경우

testAnnotationAspectWithException() 함수는 대상 메소드에 오류가 발생한 사례를 보여준다. egovframework.rte.fdl.aop.sample 패키지에 속하는 AnnotationAdviceSample 클래스의 someMethod() 메소드는 before, after throwing, after finally, around 충고(Advice)가 적용된다.

public class AdviceTest{
   @Resource(name = "adviceSample")
   AdviceSample adviceSample;

   @Test
   public void testAdviceWithException() throws Exception {
       SampleVO vo = new SampleVO();
       // exception 을 발생시키도록 플래그 설정
       vo.setForceException(true);
       ...
       try {
           // vo 의 forceException 플래그가 true 이면 - / by zero 상황을 강제로 처리함
           resultStr = adviceSample.someMethod(vo);
           fail("exception 을 강제로 발생시켜 이 라인이 수행될 수 없습니다.");
       } catch (Exception e) {
           ...
       }
   }
}

테스트 코드를 수행한 결과 로그는 다음과 같다.

AdviceUsingXML.beforeTargetMethod executed.
AdviceSample.someMethod executed.
AdviceUsingXML.aroundTargetMethod start.
AdviceUsingXML.afterThrowingTargetMethod executed.
에러가 발생했습니다.
java.lang.ArithmeticException: / by zero
...

콘솔 로그 출력을 보면 충고(Advice)가 적용되는 순서는 다음과 같다.

  • @Before
  • @Around (대상 메소드 수행 전)
  • 대상 메소드 (ArithmeticException 예외가 발생한다)
  • @AfterThrowing
  • @After(finally)

예외가 발생하더라도 after 로 정의한 충고(Advice)는 수행되는 것을 확인할 수 있다. After Throwing 충고(Advice)는 에러 메시지를 재설정하고 새로운 예외를 생성하여 전달할 수 있다.

참고자료

1.20 - 실행환경 AOP 가이드라인

표준프레임워크 실행환경은 XML Schema 기반의 AOP 방법을 사용하여 예외처리와 트랜잭션을 처리하며, 이는 @AspectJ Annotation 기반보다 횡단 관심사의 설정관계를 파악하는 데 유리하다.

실행환경 AOP 가이드라인

개요

전자정부 실행환경은 XML Schema에 기반한 AOP 방법을 사용하며, 예외처리와 트랜잭션 처리에 적용하였다. XML Schema에 기반한 AOP 방법은 @AspectJ Annotation 기반 방법에 비해 횡단 관심사에 대한 설정관계를 파악하기 유리하다.

설명

예외 처리

실행환경은 DAO에서 발생한 Exception을 받아 Service단에서 처리할 수 있다. 실행환경에서 추가로 제공하는 Exception은 다음과 같다.

  • EgovBizException: 업무에서 Checked Exception인 경우에 공통으로 사용하는 Exception이다. 개발자가 특정한 오류에 대해서 throw하여 특정 메시지를 전달하고자 하는 경우에는 processException() 메소드를 이용하도록 한다.
  • ExceptionTransfer: AOP 기능을 이용하여 ServiceImpl 클래스에서 Exception이 발생한 경우(after-throwing인 경우)에 trace()메소드에서 처리한다. 내부적으로 EgovBizException인지 RuntimeException(ex.DataAccessException)인지 구분하여 throw한다. ExceptionTransfer는 내부적으로 DefaultExceptionHandleManager 클래스에 의해서 정의된 패턴에 대해서 Handler에 의해서 동작한다.

관점(Aspect) 정의

예외 처리를 위한 Spring 설정 파일(resources/egovframework.spring/context-aspect.xml) 내에 관점(Aspect) 클래스를 빈으로 정의한 뒤, 해당 관점(Aspect)에 대한 포인트컷과 충고(Advice)를 정의한다.

<bean id="exceptionTransfer" class="org.egovframe.rte.fdl.cmmn.aspect.ExceptionTransfer">
...
</bean>
 
<aop:config>
	<aop:pointcut id="serviceMethod" expression="execution(* org.egovframe.rte.sample.service..*Impl.*(..))" />
		<aop:aspect ref="exceptionTransfer">
		<aop:after-throwing throwing="exception" pointcut-ref="serviceMethod" method="transfer" />
	</aop:aspect>
</aop:config>
...
</beans>

ExceptionTransfer는 org.egovframe.rte.sample.service 패키지 내에 속한 모든 클래스 중 클래스명이 Impl로 끝나는 클래스의 메소드 실행시 발생한 예외를 처리하는 역할을 수행한다.

충고(Advice) 정의

충고(Advice)로 정의된 ExceptionTransfer 클래스는 실행환경 소스코드에 포함되어 있다. ExceptionTransfer 클래스는 예외가 발생된 경우 내부적으로 예외처리 설정 파일에 명시된 ExceptionHandler를 호출하는 기능을 한다. ExceptionTransfer 클래스의 코드 일부는 다음과 같다.

public class ExceptionTransfer {
	...
	public void transfer(JoinPoint thisJoinPoint, Exception exception) throws Exception {
		log.debug("execute ExceptionTransfer.transfer ");
 
		Class clazz = thisJoinPoint.getTarget().getClass();
		Locale locale = LocaleContextHolder.getLocale();
 
		// BizException 인 경우는 이미 메시지 처리 되었음. 로그만 기록
		if (exception instanceof EgovBizException) {
			log.debug("Exception case :: EgovBizException ");
 
			EgovBizException be = (EgovBizException) exception;
			getLog(clazz).error(be.getMessage(), be.getCause());
			// Exception Handler 에 발생된 Package 와 Exception 설정.
			processHandling(clazz, exception, pm, exceptionHandlerServices, false);
			throw be;
		} else if (exception instanceof RuntimeException) {
			log.debug("RuntimeException case :: RuntimeException ");
 
			RuntimeException be = (RuntimeException) exception;
			getLog(clazz).error(be.getMessage(), be.getCause());
			// Exception Handler 에 발생된 Package 와 Exception 설정.
			processHandling(clazz, exception, pm, exceptionHandlerServices, true);
 
			if (be instanceof DataAccessException) {
				log.debug("RuntimeException case :: DataAccessException ");
				DataAccessException sqlEx = (DataAccessException) be;
				// throw processException(clazz, "fail.data.sql",
				// new String[] {
				// Integer.toString(((SQLException) sqlEx
				// .getCause()).getErrorCode()),
				// ((SQLException) sqlEx.getCause())
				// .getLocalizedMessage() }, sqlEx, locale);
				throw sqlEx;
			}
			throw be;
		} else if (exception instanceof FdlException) {
			log.debug("FdlException case :: FdlException ");
			FdlException fe = (FdlException) exception;
			getLog(clazz).error(fe.getMessage(), fe.getCause());
			throw fe;
		} else {
			og.debug("case :: Exception ");
			getLog(clazz).error(exception.getMessage(), exception.getCause());
			throw processException(clazz, "fail.common.msg", new String[] {}, exception, locale);
		}
	}
}

트랜잭션 처리

실행환경에서 트랜잭션 설정은 “resources/egovframework.spring/context-transaction.xml” 파일을 참조한다. 다음은 context-transaction.xml 설정 파일을 일부이다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	...
	http://www.springframework.org/schema/tx
	http://www.springframework.org/schema/tx/spring-tx.xsd
	http://www.springframework.org/schema/aop
	http://www.springframework.org/schema/aop/spring-aop.xsd">
 
	<!-- 트랜잭션 관리자를 설정한다.  -->
	<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"/>
	</bean>
 
	<!-- 트랜잭션 Advice를 설정한다. -->
	<tx:advice id="txAdvice" transaction-manager="txManager">
		<tx:attributes>
			<tx:method name="*" rollback-for="Exception"/>
		</tx:attributes>
	</tx:advice>
 
	<!-- 트랜잭션 Pointcut를 설정한다.--->
	<aop:config>
		<aop:pointcut id="requiredTx" expression="execution(* org.egovframe.rte.sample..impl.*Impl.*(..))"/>
		<aop:advisor advice-ref="txAdvice" pointcut-ref="requiredTx" />
	</aop:config>
</beans>
  • txAdvice는 메소드에서 예외 발생시 트랜잭션 롤백을 수행한다.
  • requiredTx는 egovframework.rte.sample 패키지 하위 impl 패키지에서 Impl로 끝나는 모든 클래스의 메소드를 포인트컷으로 지정한다.

참고자료

1.21 - Resource 서비스

리소스를 활용하여 가장 많이 사용하는 메시지 제공 서비스는 미리 정의된 파일에서 키값에 해당하는 메시지를 읽어 오류나 안내 메시지를 제공하는 기능을 한다.

Resource 서비스

개요

리소스를 활용하여 가장 많이 사용하는 메시지 제공 서비스를 살펴본다. 메시지 제공 서비스는 미리 정의된 파일에서 메시지를 읽어 들인 후, 오류 발생시 또는 안내 메시지를 제공하기 위해 키값에 해당하는 메시지를 가져오는 기능을 제공한다.

설명

Message Basic

메시지를 활용하기 위한 기본 설정 및 활용에 대해서 예제를 중심으로 설명한다.

Configuration

<bean name="messageSource"  class="org.springframework.context.support.ResourceBundleMessageSource">
   <property name="useCodeAsDefaultMessage">
      <value>true</value>
   </property>
   <property name="basenames">
      <list>
         <value>egovframework-message</value>
      </list>
   </property>
</bean>

위의 설정에서”egovframework-message” 로 지정한 파일은 실제로는 egovframework-message.properties 로 정의되어 있다. 파일의 위치를 지정하는 방법이 여러가지가 가능한데 그 설정에 대한 것은 4.참고자료 참조.

Sample Source

//egovframework-message.properties에 정의된 메시지 내용.
resource.basic.msg1=message1

@Resource(name="messageSource")
MessageSource messageSource ;

String getMsg = messageSource.getMessage("resource.basic.msg1" , null , Locale.getDefault() );
assertEquals("Get Message Success!", getMsg , "message1");

위의 소스를 보면 messageSource.getMessage를 이용하여 Massage를 얻는 것을 확인 할 수 있다.

Message Locale

동일한 메시지 키를 가지고 언어별로 별도로 설정 관리하여 사용자에 따라서 사용자에 맞는 언어로 메시지를 제공할 수 있다.

Configuration

<bean name="messageSource" 
   class="org.springframework.context.support.ResourceBundleMessageSource">
   <property name="useCodeAsDefaultMessage">
      <value>true</value>
   </property>
   <property name="basenames">
      <list>
         <value>egovframework-message-locale</value>		
      </list>
   </property>
</bean>

위의 설정에서”egovframework-message-locale” 로 지정한 파일을 egovframework-message-locale_ko.properties,egovframework-message-locale_en.properties로 정의하고 동일한 메시지키에 해당하는 메시지를 달리 지정한다.

Propreties File

//egovframework-message-locale_ko.properties 파일 내용
resource.locale.msg1=메시지1
 
//egovframework-message-locale_en.properties 파일 내용
resource.locale.msg1=en_message1

위에서 resource.locale.msg1 라는 키에 다른 메시지를 설정한 것을 확인할 수 있다. 위와 같이 설정하면 locale 정보에 따라서 메시지를 제공받을 수 있다.

Sample Source

//egovframework-message.properties에 정의된 메시지 내용.
resource.basic.msg1=message1
 
String getMsg = messageSource.getMessage("resource.locale.msg1" , null , Locale.KOREAN );
assertEquals("Get Message Success!", getMsg , "메시지1");
 
String getMsg = messageSource.getMessage("resource.locale.msg1" , null , Locale.ENGLISH );
assertEquals("Get Message Success!", getMsg , "en_message1");

위에서 Locale정보에 따라서 추출되는 메시지의 내용이 다른 것을 확인할 수 있다.

Message Parameter

프로그램 수행중에 발생되는 메시지를 추가하여 제공 할 수 있는데 그것에 대한 사용 방법은 설정은 위와 동일하고 Properties 파일에 아래와 같이 설정한다.

Properties File

resource.basic.msg3=message {0} {1}

위에서 {0},{1}로 정의된 부분에 추가 메시지를 입력하여 제공 받을 수 있다. 위의 설정을 활용하는 샘플은 아래와 같다.

Sample Source

 Object[] parameter = { new String("1") , new Integer(2) };
 
String getMsg = messageSource.getMessage("resource.basic.msg3" , parameter , Locale.getDefault() );
assertEquals("Get Message Success!", getMsg , "message 1 2");

위에서 parameter에 1과 2를 지정하여 getMessage의 두번째 인자에 넣고 호출하면 리턴 메시지로 “message 1 2”를 얻는 것을 확인 할 수 있다.

참고자료

1.22 - Spring Expression Language(SpEL)

Spring 3.0에서 도입된 SpEL은 빈 오브젝트에 접근해 프로퍼티 값을 동적으로 가져오는 표현식 언어로, JSP에서도 <spring:eval> 태그로 적용할 수 있다.

Spring Expression Language(SpEL)

개요

Spring 3.0에서 처음 소개된 스프링 전용 표현식 언어로 강력하고 유연하게 사용된다.
SpEL은 빈 오브젝트에 직접 접근할 수 있는 표현식을 이용해서 프로퍼티 값을 능동적으로 가져오는 방법이며 가장 기본적이다. 또한 jsp에서 <spring:eval>태그를 사용하여 SpEL을 적용 할 수도 있다.

설명

빈 설정파일을 사용하여 SpEL적용

빈 프로퍼티에 값을 설정하면, 다른 빈이나 프로퍼티에 접근 가능하다.

  • 다음의 빈에서 접근하는 예제이다.
<bean id="springTest" ..>
	<property name="test" value="Sample" />
</bean>
 
<bean id="testNames">
	<property name="name" value="#{springTest.test}" />
</bean>
  • 다음은 <util:properties> 를 사용하여 프로퍼티에 접근하는 예제 이다.

globals.properties

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:1623/EASYCOMPANY
username=tex
password=texAdmin

context-datasource.xml

<util:properties id="dbprops" location="classpath:/egovframework/property/globals.properties" />
 
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
	<property name="driverClassName" value="#{dbprops['driverClassName']}"/>
	<property name="url" value="#{dbprops['url']}"/>
	<property name="username" value="#{dbprops['username']}"/>
	<property name="password" value="#{dbprops['password']}"/>
</bean>
  • 다음은 <util:properties> 를 사용하여 변수로 직접 주입하는 예제 이다.
@Value("#{dbprops.driverClassName}")
private String driverClassName;

또는

@Value("#{dbprops}")
private Dbproperies dbprops;

JSP에서 SpEL적용

JSP의 EL대신에 Spring 3.0의 SpEL을 사용해서 값을 출력할 수 있다. JSP에서 SpEL을 사용하려면 태그 라이브러리를 추가해야 한다.

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>

<spring:eval> 태그를 사용하여 JSP에서 SpEL을 사용한다. 모델 오브젝트를 직접 사용할 수 있다.

<spring:eval expression="sampleVO.money"/>

메소드의 리턴값이 스트링일 경우, 메소드 자체를 호출할 수 있다.

<spring:eval expression="sampleVO.toString()"/>

또한, @NumberFormat, @DateTimeFormat과 같은 컨버전 서비스에 등록되는 포맷터를 자동으로 적용할 수 있다. 다음은 sampleVO의 일부이다.

/** 잔액 */
@NumberFormat(pattern = "###,##0")
private Integer money;
<spring:eval expression="sampleVO.money"/>

위와 같이 적용하면 입력 값에 따라 3자리마다 쉼표(,)가 출력된다.

입력값: 1234000 , 출력값: 1,234,000

모델에 직접 어노테이션으로 설정하지 않아도 new를 이용해 SpEL을 적용할 수 있다.

<spring:eval expression='new java.text.DecimalFormat("###,##0").format(price)'/>

image image

참고자료

2 - 공통기반

공통기반 서비스는 실행환경 서비스 간에 공통적으로 사용되는 기능을 제공한다.

공통기반

공통기반 서비스는 실행환경 서비스 간에 공통적으로 사용되는 기능을 제공한다.

2.1 - Server Security Service

Server Security Service는 Spring Security를 확장하여 사용자 인증과 권한 관리를 DB 기반으로 처리하며, 세션 관리도 지원한다. Spring Security는 인증, 권한 처리, 웹 및 서비스 레이어 보안을 제공하는 강력한 솔루션이지만 사용자 관리와 역할 관리에서 일부 취약점을 가진다. 표준프레임워크 3.0에서는 설정 간소화 기능과 Map 기반 UserDetails로 손쉬운 사용자 정보 관리가 가능해졌으며, 업그레이드 가이드를 통해 최신 보안 기능을 적용할 수 있다.

Server Security Service

개요

웹을 통해 데이터를 주고받는 업무를 진행할 경우, 보안상의 문제가 발생하기 쉽다.
Security Service는 웹을 통한 서비스 이용 시 발생할 수 있는 다양한 보안상의 취약점들을 사전에 인지하고 대응함으로써, 서비스의 안정성을 확보한다.
Security Service는 사용자 정보를 DB에서 관리하여 인증을 거쳐야만 접근할 수 있는 Authentication과 사용자 권한 정보를 계층화시켜서 화면 및 페이지, 또는 메소드에 접근할 수 있는 Authorization이 포함된다.

설명

Server Security Service는 Spring Framework의 Spring Security를 확장하여 구현하였으며, 사용자 인증정보 및 권한정보를 DB에서 관리하고, Spring Security의 UserDetails 인터페이스를 확장하여 세션정보를 담을 수 있다.
Server Security의 주요기능은 다음과 같다.

  • 자원(url, method 등) 접근 제한
  • 사용자 인증 확인
  • 미인증시 인증확인 요청
  • 계층적 권한 설정 및 사용자 권한 확인

Spring Security 강점

  • Spring Security 는 엔터프라이즈 어플리케이션을 위한 인증(Authentication), 권한 처리(Authorization) 서비스를 제공하는 강력하고 유연한 보안 솔루션이다.
  • Servlet Filter 와 Java AOP 를 통하여 보안을 강제하며 Spring의 IoC 의 lifecycle 기반으로 동작한다.
  • authentication, Web URL authorization, Method 호출 authorization, 도메인 객체 기반의 security 처리, 채널 보안(https 강제) 등의 주요 기능을 제공한다.
  • Web request 보안에 더하여 Service Layer 및 인스턴스 수준의 보안 제공으로 Layering issue 해결 및 웹 클라이언트 외의 다양한 rich 클라이언트/웹서비스에 대한 보안 제어를 지원한다.
  • 재사용성, 이식성, 코드 품질, 레퍼런스 (정부,은행,대학,기업 등 많은 business field), 다양한 타 프레임워크를 지원하며 community가 활성화 되어있다.

Spring Security 기능 취약 부분

  • 사용자관리 기능
  • 역할 관리 기능
  • XML 기반(설정 어려움)의 권한 체크

SI 프로젝트의 보안.인증/권한처리의 일반적인 요구사항

  • RDB 기반의 인증 또는 상용 SSO 연계
  • 사용자 정보의 쉽고 빠른 참조를 위한 session 사용
  • 부서, 사용자 및 메뉴/화면/권한 관리 - 개발자가 아닌 최종 사용자가 GUI 기반 관리 기능을 통해 작업하기를 원함
  • 계층 구조의 부서관리, 사용자 관리, 권한 복제, 권한 상속, 프로젝트 업무 규모에 따른 방대한 사용자/역할 데이터의 관리 필요
  • Portal solution / X-internet 도입 등 - 사용자관리/메뉴-권한 처리에 대한 유연한 통합(integration) 필요
  • 부서/사용자/메뉴/화면/권한처리 등 각 프로젝트별 요구사항이 유사하면서도 달라 중복개발의 위험성 - 일반 업무보다 난이도가 높은 공통 업무 성격. 표준화, 손쉬운 customizing, 유연한 확장이 필요

기대효과

  • 전자정부개발프레임워크의 Server Security에서 Spring Security 강점과 SI 프로젝트 활용성을 두루 갖춘 유연하고 강력한 Security 프레임워크를 확보할 수 있다.

표준프레임워크 3.0 변경사항

  • 설정간소화 기능(XML Schema 기반)을 통하여 기존 복잡한 설정을 단순화할 수 있다.
  • Map 기반의 UserDetails 사용을 통해 손쉬운 사용자정보 관리가 가능하다.

Server Security

Server Security 업그레이드 가이드

참고자료

2.2 - Architecture

전자정부 개발프레임워크의 Spring Security는 DB 기반의 실시간 인증을 사용하며, 주요 구성요소로 Scheduler, Job, JobDetail, Trigger가 있다. Security 설정은 DelegatingFilterProxy를 통해 모든 웹 요청을 관리하며, 사용자 테이블과 권한 테이블을 포함한 여러 테이블이 보안 정보 관리에 사용된다. Spring과 Quartz의 통합으로 스케줄링된 작업을 관리하며, 다양한 설정 파일을 통해 자원과 역할을 보호한다.

Architecture

개요

전자정부 개발프레임워크의 Spring Security 기본구조와 기본 환경 설정을 설명한다.
전자정부 개발프레임워크의 Server Security는 컨테이너 기동시 적용되는 XML기반 인증이 아닌 실시간 적용되는 DB기반의 JDBC 인증을 사용한다.

설명

Spring Security 아키텍처

웹어플리케이션 인증절차

image

  • 리소스 요청

  • 요청에 대해 보호되고 있는 자원인지 판단

  • 아직 인증이 안되었으므로 HTTP 응답코드(오류) 또는 특정 페이지로 redirect

  • 인증 메커니즘에 따라 웹 페이지 로그인 폼 또는 X509 인증서

  • 입력 폼의 내용을 HTTP post 또는 인증 세부사항을 포함하는 HTTP 헤더를 서버로 요청

  • 신원정보(credential)가 유효한지 판단

  • 유효한 경우 다음단계 진행

  • 유효하지 않을 경우 신원정보 재요청(되돌아감)

  • 보호 자원의 접근 권한이 있을 경우 요청 성공 / 접근 권한이 없을 경우 forbidden 403 HTTP 오류

Spring Security Filter Chain

server security Filter Chain 흐름

server security Filter Chain 흐름 (HTTP요청)

server security Authentication Processing Filter

  • Spring Security에서 생성한 정보 - SecurityContextHoler를 이용하여 SecurityContext 얻음
  • 응용프로그램이 분산되어 있는 경우 등 다양한 환경에서 사용 가능토록 SecurityContext를 SecurityContextHolder 내부에 생성한 ThreadLocal 객체를 이용하여 저장하고 있음.
  • ThreadLocal 객체는 현재 쓰레드에서 필요한 상태 정보만 담을 수 있음.
  • 웹 환경에서 요청이 있을때 마다 동일한 역할을 하는 SecurityContext를 다시 생성하는 것은 맞지 않음 → httpSessionContextIntegrationFilter 를 사용하여 SecurityContext 정보를 ThreadLocal에 기록하고 가져오는 작업을 수행함. (Session 에 저장)

Security Configuration

필수라이브러리

  • spring-security-core-2.0.4.jar
  • spring-security-taglibs-2.0.4.jar

web.xml 등록

  • org.springframework.web.filter.DelegatingFilterProxy 등록 : Application Context에 Spring bean으로 등록된 필터 구현체를 대표하는 Spring Framework 클래스이다.
  • 모든 웹요청이 Spring Security의 DelegatingFilterProxy로 전달되도록 한다.
  • DelegatingFilterProxy는 웹요청이 서로 다른 URL 패턴에 근거하여 서로 다른 필터로 전달될 수 있도록 해주는 일반적으로 사용할 수 있는 클래스이다.
  • 이런 위임된 필터들은 어플리케이션 컨텍스트 내에서 관리되며, 따라서 의존성 주입의 이점을 누릴 수 있다.
example
<filter>
	<filter-name>springSecurityFilterChain</filter-name>
	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
	<filter-name>springSecurityFilterChain</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

주요테이블

사용자 인증과 관련된 테이블은 사용자테이블과 사용자권한테이블이며 사용자권한관련 테이블은 역할, 자원, 역할계층 등의 테이블이 있다.

image

DaoAuthenticationProvidor

사용자 테이블
CREATE TABLE USERS (
	USERNAME VARCHAR(50) NOT NULL,
	PASSWORD VARCHAR(50) NOT NULL,
	ENABLED BIT NOT NULL,
	CONSTRAINT PK_USERS PRIMARY KEY(USERNAME)
);
  • 필수 필드 : USERNAME(사용자 ID), PASSWORD(사용자 암호), ENABLED(계정 사용여부)
  • 나머지 필드 : 세션처리를 위해 사용자 테이블의 나머지 정보를 사용한다. (예. 주민등록번호, 주소, 생일 ..)
사용자 권한 테이블
CREATE TABLE AUTHORITIES (
	USERNAME VARCHAR(50) NOT NULL,
	AUTHORITY VARCHAR(50) NOT NULL,
	CONSTRAINT PK_AUTHORITIES PRIMARY KEY(USER_ID,AUTHORITY),
	CONSTRAINT FK_USERS FOREIGN KEY(USER_ID) REFERENCES USERS(USER_ID),
	CONSTRAINT FK_ROLES3 FOREIGN KEY(AUTHORITY) REFERENCES ROLES(AUTHORITY)
);
  • 필수 필드 : USER_ID(사용자 ID), AUTHORITY(권한)
역할 테이블
CREATE TABLE ROLES (
	AUTHORITY VARCHAR(50) NOT NULL,
	ROLE_NAME VARCHAR(50),
	DESCRIPTION VARCHAR(100),
	CREATE_DATE DATE,
	MODIFY_DATE DATE,
	CONSTRAINT PK_ROLES PRIMARY KEY(AUTHORITY)
);
AUTHORITYDESCRIPTION
IS_AUTHENTICATED_ANONYMOUSLY익명 사용자
IS_AUTHENTICATED_REMEMBEREDREMEMBERED 사용자
IS_AUTHENTICATED_FULLY인증된 사용자
ROLE_RESTRICTED제한된 사용자
ROLE_USER일반 사용자
ROLE_ADMIN관리자
ROLE_AA 업무
ROLE_BB 업무
역할 계층 테이블

역할의 계층구조를 저장하는 테이블

CREATE TABLE ROLES_HIERARCHY (
	PARENT_ROLE VARCHAR(50) NOT NULL,
	CHILD_ROLE VARCHAR(50) NOT NULL,
	CONSTRAINT PK_ROLES_HIERARCHY PRIMARY KEY(PARENT_ROLE,CHILD_ROLE),
	CONSTRAINT FK_ROLES1 FOREIGN KEY(PARENT_ROLE) REFERENCES ROLES(AUTHORITY),
	CONSTRAINT FK_ROLES2 FOREIGN KEY(CHILD_ROLE) REFERENCES ROLES (AUTHORITY)
);
CHILD_ROLEPARENT_ROLE
ROLE_ADMINROLE_USER
ROLE_USERROLE_RESTRICTED
ROLE_RESTRICTEDIS_AUTHENTICATED_FULLY
IS_AUTHENTICATED_FULLYIS_AUTHENTICATED_REMEMBERED
IS_AUTHENTICATED_REMEMBEREDIS_AUTHENTICATED_ANONYMOUSLY
ROLE_ADMINROLE_A
ROLE_ADMINROLE_B
ROLE_AROLE_RESTRICTED
ROLE_BROLE_RESTRICTED
보호된 자원 테이블
CREATE TABLE SECURED_RESOURCES (
	RESOURCE_ID VARCHAR(10) NOT NULL,
	RESOURCE_NAME VARCHAR(50),
	RESOURCE_PATTERN VARCHAR(300) NOT NULL,
	DESCRIPTION VARCHAR(100),
	RESOURCE_TYPE VARCHAR(10),
	SORT_ORDER INTEGER,
	CREATE_DATE DATE,
	MODIFY_DATE DATE,
	CONSTRAINT PK_RECURED_RESOURCES PRIMARY KEY(RESOURCE_ID)
);

url, method, pointcut으로 자원을 보호한다.

RESOURCE_IDRESOURCE_PATTERN
web-000001\A/test\.do\Z
web-000002\A/sale/.*\.do\Z
web-000003\A/cvpl/((?!EgovCvplLogin\.do).)*\Z
mtd-000001egovframework.rte.sample.service.EgovSampleService.updateSample
mtd-000002egovframework.rte.sample.service.EgovSampleService.deleteSample
mtd-000003execution(* egovframework.rte.sample..service.*Service.insert*(..))
보호된 자원 역할 테이블

보호된 자원과 역할과의 매핑 테이블

CREATE TABLE SECURED_RESOURCES_ROLE (
	RESOURCE_ID VARCHAR(10) NOT NULL,
	AUTHORITY VARCHAR(50) NOT NULL,
	CONSTRAINT PK_SECURED_RESOURCES_ROLE PRIMARY KEY(RESOURCE_ID,AUTHORITY),
	CONSTRAINT FK_SECURED_RESOURCES FOREIGN KEY(RESOURCE_ID) REFERENCES SECURED_RESOURCES(RESOURCE_ID),
	CONSTRAINT FK_ROLES4 FOREIGN KEY (AUTHORITY) REFERENCES ROLES(AUTHORITY)
);

참고자료

2.3 - Authentication

전자정부 표준프레임워크의 인증은 DB 기반의 JDBC 인증을 사용하며, Spring Security의 AuthenticationManagerAuthenticationProvider를 설정하여 사용한다. 최소한의 환경설정으로 XML 또는 DB에서 사용자 정보를 관리할 수 있으며, JDBC 인증을 통해 사용자 정보와 권한을 쿼리로 관리한다. 또한, 세션 관리를 통해 사용자 세션을 확장하고, 동시 세션 제어를 통해 동일한 ID로의 동시 접속을 제한할 수 있다.

Authentication

개요

허락된 사용자에게만 공개되는 컨텐츠(정보 또는 기능)에 접근하기 위해 반드시 아이디와 암호를 입력하는 로그인 과정을 거치는데 이러한 과정이 인증(authentication)이다.
즉, 인증은 특정 사용자가 유효한 사용자인지를 판단하는 과정을 의미한다.
본 가이드에서는 인증을 위한 기본적인 환경 및 전자정부 표준프레임워크에서 사용된 인증 방법을 설명한다.

설명

전자정부 표준프레임워크의 인증은 XML기반의 인증이 아닌 DB기반의 JDBC인증을 사용한다.
기본적인 인증 메커니즘은 인증 주체가 인증을 시도하는 초기에 오직 한 번만 인증 메커니즘이 사용되며 그 이후로는 인증 메커니즘이 정보를 필터에 유지하여 요구되는 요청을 필터 체인상의 다음 필터로 전달하기만 한다.
Spring Security에서 제공하는 인증을 사용하기 위해서는 AuthenticationManager와 실제 인증에 대한 정보를 제공하는 AuthenticationProvider의 설정이 필요하다.
AuthenticationManager는 요청을 AuthenticationProvider 체인에 전달해야 할 임무가 있다.
AuthenticationProvider는 기본적으로 UserDetails와 UserDetailsService인터페이스를 이용한다.
UserDetailsService 인터페이스 내 loadUserByUsername 메소드의 리턴된 UserDetails은 사용자명(username), 패스워드(password), 허가권한(GrantedAuthority[]) 및 사용여부(enabled)와 같은 기초적인 인증 정보들을 제공한다.
이에 전자정부 표준프레임워크에서는 UserDetails 인터페이스를 확장하였으며 JDBC 기반으로 사용자 테이블로부터 사용자 정보를 Map 또는 VO형태로 사용할 수 있도록 하였다.

최소환경설정

최소환경설정으로 사용자 인증을 설정하는 예제이다.

Configuration
<http>
	<intercept-url pattern="/**" access="ROLE_USER"/>
	<form-login />
	<logout />
</http>
  • 은 여러 개를 정의할 수 있으며, 위에서부터 차례로 해당되는 url pattern이 적용된다. 정규식을 사용하여 접근 url을 정의한다. “access”속성은 접근할 수 있는 사용자 권한을 나타낸다.
  • 을 지정하지 않을 경우나 기본 형식()으로 등록된 경우 Spring Security에서 제공하는 기본 로그인 폼을 사용한다.

인증요청을 처리하기 위해 authentication manager가 xml(또는 DB, LDAP 등)에 정의된 사용자 정보를 사용한다.

<authentication-manager>
	<authentication-provider>
		<user-service>
			<user name="jimi" password="jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
			<user name="bob" password="bobspassword" authorities="ROLE_USER" />
		</user-service>
	</authentication-provider>
</authentication-manager>
  • user-service : 사용자 정보(사용자 ID, 사용자 암호, 권한 등)를 얻어오는 서비스

JDBC 인증

표준프레임워크에서는 JdbcUserDetailsManager(org.springframework.security.provisioning)를 통해 인증 처리 부분을 JDBC 방식으로 확장하였다.

관련된 설정은 다음과 같이 지정한다.

Configuration
<authentication-manager>
	<authentication-provider user-service-ref="jdbcUserService">
		<password-encoder hash="sha-256" base64="true"/>
	</authentication-provider>		
</authentication-manager>
 
 
<beans:bean id="jdbcUserService" class="egovframework.rte.fdl.security.userdetails.jdbc.EgovJdbcUserDetailsManager" >
	<beans:property name="usersByUsernameQuery" value="SELECT USER_ID,PASSWORD,ENABLED,USER_NAME,BIRTH_DAY,SSN FROM USERS WHERE USER_ID = ?"/>
	<beans:property name="authoritiesByUsernameQuery" value="SELECT USER_ID,AUTHORITY FROM AUTHORITIES WHERE USER_ID = ?"/>
	<beans:property name="roleHierarchy" ref="roleHierarchy"/>
	<beans:property name="dataSource" ref="dataSource"/>
	<beans:property name="mapClass" value="egovframework.rte.fdl.security.userdetails.EgovUserDetailsMapping"/>
</beans:bean>
  • usersByUsernameQuery : 사용자 인증을 위한 query로 처음 3개 column은 인증시 사용되는 정보로 각각 user id, 패스워드, 활성화 여부를 나타낸다. 나머지 column의 경우 인증시에는 사용되지 않지만, 지정된 mapClass에 의해 사용되어 인증된 사용자 정보라 이후 참조될 수 있다.
  • authoritiesByUsernameQuery : 인증된 사용자에게 부여되는 role(authorites)로 user id와 부여된 authority 2개의 column 지정이 필요하다.

※ 기타 설정들은 하단 “세션관리” 참조

HTTP Form 인증 메카니즘

HTTP 폼 인증은 AuthenticationProcessingFilter를 이용하여 로그인 폼을 처리하는 것을 수반한다.
HTTP 폼 인증은 어플리케이션에서 가장 널리 사용되는 최종 사용자에 대한 인증 방법이다.
로그인 폼은 단순히 j_username과 j_password 입력 필드를 포함하며, 필터에 의해 모니터링되고 있는 URL로 게시한다.기본값은 j_spring_security_check 이다.

기타 인증

BASIC 인증
다이제스트 인증
익명 인증
Remember-Me 인증
X509 인증
LDAP 인증
CAS 인증
컨테이너 어댑터 인증

Sample Configuration

WEB Security service
<http pattern="/css/**" security="none"/>    
<http pattern="/images/**" security="none"/>
<http pattern="/js/**" security="none"/>
<http pattern="A/WEB-INF/jsp/.*Z" request-matcher="regex" security="none"/> 	
 
<http access-denied-page="/system/accessDenied.do" request-matcher="regex">
	<form-login login-processing-url="/j_spring_security_check"
		authentication-failure-url="/cvpl/EgovCvplLogin.do?login_error=1"
		default-target-url="/index.jsp?flag=L"
		login-page="/cvpl/EgovCvplLogin.do" />
	<anonymous/>
	<logout logout-success-url="/cvpl/EgovCvplLogin.do"/>
 
	<!-- for authorization -->
	<custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="filterSecurityInterceptor"/>
</http>
  • <http> 설정으로 허용된 사용자만 접근이 가능하게 할 수 있다.
  • <form-login> 을 지정할 경우 개발자가 직접 지정한 로그인 화면 등을 지정할 수 있다.
	<authentication-manager>
		<authentication-provider user-service-ref="jdbcUserService">
			<password-encoder  hash="sha-256" base64="true"/>
		</authentication-provider>		
	</authentication-manager>
  • 사용자 정보(사용자 ID, 사용자 암호, 권한 등)를 얻어오는 기능은 jdbcUserService 에서 담당한다.
  • password-encoder : 저장된 패스워드의 암호화 알고리즘은 sha-256를 사용한다.
JDBC User Service

사용자 정보를 얻어오는 user service를 DB에 저장된 사용자 정보를 이용하여 인증할 수 있다.

<jdbc-user-service id="jdbcUserService" data-source-ref="dataSource"
	users-by-username-query="SELECT USER_ID,PASSWORD,ENABLED,BIRTH_DAY FROM USERS WHERE USER_ID = ?"
	authorities-by-username-query="SELECT USER_ID,AUTHORITY FROM AUTHORITIES WHERE USER_ID = ?"/>
Sample Source
<form action="<s:url value='/j_spring_security_check'/>" method="POST">
	<table>
		<tr><td>User:</td><td><input type='text' name='j_username'></td></tr>
		<tr><td>Password:</td><td><input type='password' name='j_password'></td></tr>
		<tr><td colspan='2' align="center"><input name="submit" type="submit" value="로그인">
			         &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
                                         <input name="reset" type="reset" value="취소"></td></tr>
	</table>
</form>

세션관리

Security Service의 세션 기능은 Spring Security의 JdbcUserDetailsManager 인터페이스를 확장하여 EgovJdbcUserDetailsManager 클래스를 구현하였으며
기본 테이블에 기재된 username, password, enabled 필드 외에 다른 사용자 정보를 추가하여 세션정보를 관리할 수 있다.

세션설정

Configuration

세션 기능을 사용하기 위해서는 JDBC 인증의 환경설정 부분을 전자정부 개발프레임워크 Server Security의 환경으로 수정해야한다.
변경전(사용안함)

<jdbc-user-service id="jdbcUserService" data-source-ref="dataSource"
	users-by-username-query="SELECT USER_ID,PASSWORD,ENABLED FROM USERS WHERE USER_ID = ?"
	authorities-by-username-query="SELECT USER_ID,AUTHORITY FROM AUTHORITIES WHERE USER_ID = ?"/>

변경후(사용)

<beans:bean id="jdbcUserService" class="egovframework.rte.fdl.security.userdetails.jdbc.EgovJdbcUserDetailsManager" >
	<beans:property name="usersByUsernameQuery" value="SELECT USER_ID,PASSWORD,ENABLED,USER_NAME,BIRTH_DAY,SSN FROM USERS WHERE USER_ID = ?"/>
	<beans:property name="authoritiesByUsernameQuery" value="SELECT USER_ID,AUTHORITY FROM AUTHORITIES WHERE USER_ID = ?"/>
	<beans:property name="roleHierarchy" ref="roleHierarchy"/>
	<beans:property name="dataSource" ref="dataSource"/>
	<beans:property name="mapClass" value="egovframework.rte.fdl.security.userdetails.EgovUserDetailsMapping"/>
</beans:bean>
  • class : egovframework.rte.fdl.security.userdetails.jdbc.EgovJdbcUserDetailsManager
  • usersByUsernameQuery : 사용자 인증을 위해 사용자 테이블에서 사용자정보를 조회한다.
  • authoritiesByUsernameQuery : 사용자 인증을 위해 사용자권한 테이블에서 사용자권한정보를 조회한다.
  • roleHierarchy : 역할의 계층적 관리를 위해 계층 역할을 설정한다.
  • mapClass : 세션 사용을 위한 세션 쿼리 및 세션 VO 간의 매핑 클래스를 설정한다.
  • dataSource : JDBC 서비스를 위한 dataSource를 설정한다.
VO Class (ex)
public class EgovUserDetailsVO {
	private String userId;
	private String passWord;
	private String userName;
	private String ssn;
	private String lsYn;
	private String birthDay;
	private Integer age;
	private String cellPhone;
	private String addr;
	private String email;
 
	public String getUserId() {
		return userId;
	}
	public void setUserId(String userId) {
		this.userId = userId;
	}
	public String getPassWord() {
		return passWord;
	}
	public void setPassWord(String passWord) {
		this.passWord = passWord;
	}
	public String getUserName() {
		return userName;
	}
	public void setUserName(String userName) {
		this.userName = userName;
	}
	public String getSsn() {
		return ssn;
	}
	public void setSsn(String ssn) {
		this.ssn = ssn;
	}
 
	.
	.
	.
Mapping Class
public class EgovUserDetailsMapping extends EgovUsersByUsernameMapping {
	public EgovUserDetailsMapping(DataSource ds, String usersByUsernameQuery) {
		super(ds, usersByUsernameQuery);
	}
 
	@Override
	protected Object mapRow(ResultSet rs, int rownum) throws SQLException {
		String userid = rs.getString("user_id");
		String password = rs.getString("password");
		boolean enabled = rs.getBoolean("enabled");
 
		String username = rs.getString("user_name");
		String birthDay = rs.getString("birth_day");
		String ssn = rs.getString("ssn");
 
		EgovUserDetailsVO userVO = new EgovUserDetailsVO();
		userVO.setUserId(userid);
		userVO.setPassWord(password);
		userVO.setUserName(username);
		userVO.setBirthDay(birthDay);
		userVO.setSsn(ssn);
 
		return new EgovUserDetails(userid, password, enabled, userVO);
	}
}
  • EgovUsersByUsernameMapping 클래스 상속
  • mapRow 메소드 재정의
  • ResultSet 클래스에서 작성된 VO로 데이터 매핑
  • VO 객체를 EgovUserDetails(userid, password, enabled, userVO) 에 담아서 리턴

세션사용

세션 가져오기
import egovframework.rte.fdl.security.userdetails.util.EgovUserDetailsHelper;
  .
  .
  .
 
EgovUserDetailsVO user = 
	(EgovUserDetailsVO)EgovUserDetailsHelper.getAuthenticatedUser();
 
assertEquals("jimi",		user.getUserId());
assertEquals("jimi test",	user.getUserName());
assertEquals("19800604",	user.getBirthDay());
assertEquals("1234567890123",	user.getSsn());
인증되지 않은 경우 처리
Boolean isAuthenticated = EgovUserDetailsHelper.isAuthenticated();
assertFalse(isAuthenticated.booleanValue());

또는

assertNull(EgovUserDetailsHelper.getAuthenticatedUser());

동시 세션처리

Server security에서는 동일한 ID에 대하여 동시 접속을 제한할 수 있다.

이를 위하여 우선 web.xml에 다음과 같이 HttpSessionEventPublisher listener를 등록해 주어야 한다.

web.xml 설정

<listener>
  <listener-class>
    org.springframework.security.web.session.HttpSessionEventPublisher
  </listener-class>
</listener>

security 설정

<http access-denied-page="/system/accessDenied.do" request-matcher="regex">
	<form-login login-processing-url="/j_spring_security_check"
		authentication-failure-url="/cvpl/EgovCvplLogin.do?login_error=1"
		default-target-url="/index.jsp?flag=L"
		login-page="/cvpl/EgovCvplLogin.do" />
	<anonymous/>
	<logout logout-success-url="/cvpl/EgovCvplLogin.do"/>
 
	<session-management>
		<concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
	</session-management>
 
</http>

concurrent-session-control

  • max-sessions : 최대 허용 세션 수
  • exception-if-maximum-exceeded : true ⇒ 최대 세션 수 초과할 경우 Exception 발생 , false ⇒ 최대 세션 수 초과할 경우 강제 로그아웃

참고자료

2.4 - Authorization

전자정부 표준프레임워크의 권한 부여(Authorization)는 XML 또는 DB에서 권한을 관리하며 계층적 권한을 지원한다. Spring Security의 FilterSecurityInterceptor를 통해 보호 자원에 대한 접근 권한을 관리하고, DB 기반의 보호 자원 맵핑 정보를 동적으로 반영할 수 있다. 또한, 역할 계층은 XML 또는 DB에서 관리하며, 사용자는 세션을 통해 권한 정보를 가져와 처리할 수 있다.

Authorization

개요

웹 사이트에 존재하는 모든 사용자들은 사이트 정책에 따라 그 부류 별로 컨텐츠에 대한 접근이 제한 되는데 이것을 권한 부여(authorization)라 한다. 즉, 권한은 특정 사용자가 웹 사이트에서 제공하는 컨텐츠(정보 또는 기능)에 접근 가능한지를 판단하는 과정을 의미한다.

설명

Authorization은 XML 또는 DB에서 권한을 관리하며 계층적 권한을 지원한다.

Server Security에서는 Filter Security Interceptor에 의해 처리되며, DB로부터 권한 정보를 처리하기 위해 다음과 같이 설정된다.

<http ...>
...
	<!-- for authorization -->
	<custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="filterSecurityInterceptor"/>
</http>
 
<beans:bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">	
	<beans:property name="authenticationManager" ref="org.springframework.security.authenticationManager" />
	<beans:property name="accessDecisionManager" ref="org.springframework.security.access.vote.AffirmativeBased#0" />
	<beans:property name="securityMetadataSource" ref="databaseSecurityMetadataSource" />
</beans:bean>
...
 
<beans:bean id="databaseSecurityMetadataSource" class="egovframework.rte.fdl.security.intercept.EgovReloadableFilterInvocationSecurityMetadataSource">
	<beans:constructor-arg ref="requestMap" />	
	<beans:property name="securedObjectService" ref="securedObjectService"/>
</beans:bean>
 
<!--  url  -->
<beans:bean id="requestMap" 	class="egovframework.rte.fdl.security.intercept.UrlResourcesMapFactoryBean" init-method="init">
	<beans:property name="securedObjectService" ref="securedObjectService"/>
</beans:bean>
 
<beans:bean id="securedObjectService" class="egovframework.rte.fdl.security.securedobject.impl.SecuredObjectServiceImpl">
	<beans:property name="securedObjectDAO" ref="securedObjectDAO"/>
	<beans:property name="requestMatcherType" value="regex"/>	<!--  default : ant -->
</beans:bean>
 
<beans:bean id="securedObjectDAO" class="egovframework.rte.fdl.security.securedobject.impl.SecuredObjectDAO" >
	<beans:property name="dataSource" ref="dataSource"/>
</beans:bean>
  • authenticationManager : <authentication-manager> 설정의 의해 자동으로 생성되는 bean ID를 지정(“org.springframework.security.authenticationManager”)
  • accessDecisionManager : 권한 부여 여부를 결정하는 Access Decision Manager로서 자동으로 생성되기 때문에 해당 bean ID를 지정하면 됨(“org.springframework.security.access.vote.AffirmativeBased#0”)
  • securityMetadataSource : 권한 설정 정보에 대한 처리 담당 참고로 자동으로 등록되는 AccessDecisionManager는 다음과 같은 설정을 갖는다.
<beans:bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
	<beans:property name="allowIfAllAbstainDecisions" value="false" />
	<beans:property name="decisionVoters">
		<beans:list>
			<beans:bean class="org.springframework.security.access.vote.RoleVoter">
				<beans:property name="rolePrefix" value="ROLE_" />
			</beans:bean>
			<beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
		</beans:list>
	</beans:property>
</beans:bean>
  • org.springframework.security.vote.AffirmativeBased : 단 한 개의 오브젝트라도 허락하면 허가하는 방식으로 처리(기본 처리)
  • org.springframework.security.vote.UnanimousBased : 모든 오브젝트가 허락해야 허가하는 방식으로 처리
  • decisionVoters : 허가 오브젝트에 대한 목록을 설정함

자원 관리

url

요청되는 웹 url을 점검하여 DB에 저장된 url과 비교하여 접근권한을 지정할 수 있다.

\A/sale/.*\.do\Z
\A/cvpl/((?!EgovCvplLogin\.do).)*\Z
filterSecurityInterceptor

FilterSecurityInterceptor는 HTTP 자원의 보안을 처리할 책임이 있다. 다른 보안 인터셉터와 유사하게 FilterSecurityInterceptor는 AuthenticationManager와 AccessDecisionManager에 대한 참조를 필요로 한다. 또한 FilterSecurityInterceptor는 서로 다른 HTTP URL 요청에 적용되는 설정 속성도 설정한다.

<beans:bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">	
	<beans:property name="authenticationManager" ref="org.springframework.security.authenticationManager" />
	<beans:property name="accessDecisionManager" ref="org.springframework.security.access.vote.AffirmativeBased#0" />
	<beans:property name="securityMetadataSource" ref="databaseSecurityMetadataSource" />
</beans:bean>
databaseSecurityMetadataSource

DB 기반으로 현재 시점의 url 보호자원-권한의 맵핑 정보를 Runtime 에 동적으로 변경 반영하기 위한 Spring Security 의 FilterInvocationSecurityMetadataSource 확장 클래스이다.

<beans:bean id="databaseSecurityMetadataSource" class="egovframework.rte.fdl.security.intercept.EgovReloadableFilterInvocationSecurityMetadataSource">
	<beans:constructor-arg ref="requestMap" />	
	<beans:property name="securedObjectService" ref="securedObjectService"/>
</beans:bean>
requestMap

DB 기반의 보호자원 맵핑 정보를 얻어 이를 참조하는 빈의 초기화 데이터로 제공한다. securedObjectService의 getRolesAndUrl()를 호출하여 DB에서 역할과 url의 매핑정보를 얻어온다.

<beans:bean id="requestMap" class="egovframework.rte.fdl.security.intercept.UrlResourcesMapFactoryBean" init-method="init">
	<beans:property name="securedObjectService" ref="securedObjectService"/>
</beans:bean>

method

Spring Security는 Spring의 DefaultAdvisorAutoProxyCreator와 함께 사용될 수 있는 MethodSecurityMetadataSourceAdvisor 처리를 제공하며, 이것을 이용하여 자동적으로 보안 인터셉터를 MethodSecurityInterceptor를 정의한 빈의 앞부분에 연결한다.

methodSecurityMetadataSourceAdvisor
<beans:bean id="methodSecurityMetadataSourceAdvisor" class="org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor">
	<beans:constructor-arg value="methodSecurityInterceptor" />
	<beans:constructor-arg ref="delegatingMethodSecurityMetadataSource" />
	<beans:constructor-arg value="delegatingMethodSecurityMetadataSource" />
</beans:bean>
methodSecurityInterceptor

메소드 요청에 대한 자원을 보호하기 위해 MethodSecurityInterceptor를 어플리케이션 Context에 추가해야하며 보안을 필요로 하는 빈이 인터셉터에 연결(chaining)된다. 이러한 연결은 Spring의 ProxyFactoryBean이나 BeanNameAutoProxyCreator를 이용하여 만들어지며, Spring의 여러 다른 부분들이 통상적으로 이용되는 방식과 유사하다. MethodSecurityInterceptor 는 다음과 같이 설정한다.

<beans:bean id="methodSecurityInterceptor" class="org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor">
	<beans:property name="validateConfigAttributes" value="false" />
	<beans:property name="authenticationManager" ref="org.springframework.security.authenticationManager"/>
	<beans:property name="accessDecisionManager" ref="org.springframework.security.access.vote.AffirmativeBased#0"/>
	<beans:property name="securityMetadataSource" ref="delegatingMethodSecurityMetadataSource" />
</beans:bean>
<beans:bean id="delegatingMethodSecurityMetadataSource" class="org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource">
	<beans:constructor-arg>
		<beans:list>
			<beans:ref bean="methodSecurityMetadataSources" />
			<beans:bean class="org.springframework.security.access.annotation.SecuredAnnotationSecurityMetadataSource" />
			<beans:bean class="org.springframework.security.access.annotation.Jsr250MethodSecurityMetadataSource" />
		</beans:list>
	</beans:constructor-arg>
</beans:bean>
methodSecurityMetadataSources
<beans:bean id="methodSecurityMetadataSources" class="org.springframework.security.access.method.MapBasedMethodSecurityMetadataSource">
	<beans:constructor-arg ref="methodMap" />
</beans:bean>
methodSecurityMetadataSources
<beans:bean id="methodSecurityMetadataSources" class="org.springframework.security.access.method.MapBasedMethodSecurityMetadataSource">
	<beans:constructor-arg ref="methodMap" />
</beans:bean>
methodMap

DB 기반의 보호자원 맵핑 정보를 얻어 이를 참조하는 빈의 초기화 데이터로 제공한다. resourceType을 method로 설정하여 securedObjectService의 getRolesAndMethod()를 호출하여 DB에서 역할과 메소드의 매핑정보를 얻어온다.

<beans:bean id="methodMap" class="egovframework.rte.fdl.security.intercept.MethodResourcesMapFactoryBean" init-method="init">
	<beans:property name="securedObjectService" ref="securedObjectService"/>
	<beans:property name="resourceType" value="method"/>
</beans:bean>

pointcut

DB 기반의 보호자원 맵핑 정보를 얻어 이를 참조하는 빈의 초기화 데이터로 제공한다. resourceType을 pointcut으로 설정하여 securedObjectService의 getRolesAndPointcut()를 호출하여 DB에서 역할과 Pointcut의 매핑정보를 얻어온다. ex: execution(* egovframework.rte.security..service.*Service.insert*(..))

<beans:bean id="protectPointcutPostProcessor" class="org.springframework.security.config.method.ProtectPointcutPostProcessor">
	<beans:constructor-arg ref="methodSecurityMetadataSources" />
	<beans:property name="pointcutMap" ref="pointcutMap"/>
</beans:bean>
 
<beans:bean id="pointcutMap" class="egovframework.rte.fdl.security.intercept.MethodResourcesMapFactoryBean" init-method="init">
	<beans:property name="securedObjectService" ref="securedObjectService"/>
	<beans:property name="resourceType" value="pointcut"/>
</beans:bean>

역할 관리

역할은 상하 계층으로 관리하며 어플리케이션 Context 또는 DB에 저장하여 관리한다.

XMl Context에 계층 역할을 등록하여 관리할 경우

<beans:bean id="roleHierarchy" 
		class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl" >
	<beans:property name="hierarchy">
		<beans:value>
			ROLE_ADMIN > ROLE_USER
			ROLE_USER > ROLE_RESTRICTED
			ROLE_RESTRICTED > IS_AUTHENTICATED_FULLY
			IS_AUTHENTICATED_REMEMBERED > IS_AUTHENTICATED_ANONYMOUSLY
		</beans:value>
	</beans:property>
</beans:bean>

DB에서 계층 역할을 관리할 경우

<beans:bean id="roleHierarchy" 
		class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl" >
	<beans:property name="hierarchy" ref="hierarchyStrings"/>
</beans:bean>
<beans:bean id="hierarchyStrings" class="egovframework.rte.fdl.security.userdetails.hierarchicalroles.HierarchyStringsFactoryBean" init-method="init">
	<beans:property name="securedObjectService" ref="securedObjectService"/>
</beans:bean>
	<beans:bean id="jdbcUserService" class="egovframework.rte.fdl.security.userdetails.jdbc.EgovJdbcUserDetailsManager" >
		<beans:property name="usersByUsernameQuery" value="SELECT USER_ID,PASSWORD,ENABLED,USER_NAME,BIRTH_DAY,SSN FROM USERS WHERE USER_ID = ?"/>
		<beans:property name="authoritiesByUsernameQuery" value="SELECT USER_ID,AUTHORITY FROM AUTHORITIES WHERE USER_ID = ?"/>
		<beans:property name="roleHierarchy" ref="roleHierarchy"/>
		<beans:property name="dataSource" ref="dataSource"/>
		<beans:property name="mapClass" value="egovframework.rte.fdl.security.userdetails.EgovUserDetailsMapping"/>
	</beans:bean>
  • class : egovframework.rte.fdl.security.userdetails.jdbc.EgovJdbcUserDetailsManager
  • usersByUsernameQuery : 사용자 인증을 위해 사용자 테이블에서 사용자정보를 조회한다.
  • authoritiesByUsernameQuery : 사용자 인증을 위해 사용자권한 테이블에서 사용자권한정보를 조회한다.
  • roleHierarchy : 역할의 계층적 관리를 위해 계층 역할을 설정한다.
  • mapClass : 세션 사용을 위한 세션 쿼리 및 세션VO간의 매핑 클래스를 설정한다.
  • dataSource : JDBC 서비스를 위한 dataSource를 설정한다.

Configuration

<beans:bean id="securedObjectService" class="egovframework.rte.fdl.security.securedobject.impl.SecuredObjectServiceImpl">
	<beans:property name="securedObjectDAO" ref="securedObjectDAO"/>
	<beans:property name="requestMatcherType" value="regex"/>	<!--  default : ant -->
</beans:bean>
 
<beans:bean id="securedObjectDAO" class="egovframework.rte.fdl.security.securedobject.impl.SecuredObjectDAO" >
	<beans:property name="dataSource" ref="dataSource"/>
</beans:bean>

아래의 속성쿼리는 SecuredObjectDAO 빈에 기본으로 내장되었으며 DBMS 벤더에 따른 SQL문 차이 또는 DB 스키마 차이로 인한 변경된 쿼리를 직접 반영할 수 있다. 내장된 기본 쿼리는 다음과 같다.

  • sqlHierarchicalRoles
SELECT a.child_role child, a.parent_role parent
FROM ROLES_HIERARCHY a LEFT JOIN ROLES_HIERARCHY b on (a.child_role = b.parent_role)
  • sqlRolesAndUrl
SELECT a.resource_pattern url, b.authority authority
FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
WHERE a.resource_id = b.resource_id
AND a.resource_type = 'url' ORDER BY a.sort_order
  • sqlRolesAndMethod
SELECT a.resource_pattern method, b.authority authority
FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
WHERE a.resource_id = b.resource_id
AND a.resource_type = 'method' ORDER BY a.sort_order
  • sqlRolesAndPointcut
SELECT a.resource_pattern pointcut, b.authority authority
FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
WHERE a.resource_id = b.resource_id
AND a.resource_type = 'pointcut' ORDER BY a.sort_order

SecuredObjectDAO 빈에 내장된 SQL을 사용하지 않을 경우 아래와 같이 SQL을 지정하여 설정한다.

<beans:bean id="securedObjectDAO" class="egovframework.rte.fdl.security.securedobject.impl.SecuredObjectDAO" >
	<beans:property name="dataSource" ref="dataSource"/>
	<beans:property name="sqlHierarchicalRoles">
		<beans:value>
			SELECT a.child_role child, a.parent_role parent
			FROM ROLES_HIERARCHY a LEFT JOIN ROLES_HIERARCHY b on (a.child_role = b.parent_role)
		</beans:value>
	</beans:property>
	<beans:property name="sqlRolesAndUrl">
		<beans:value>
			SELECT a.resource_pattern url, b.authority authority
			FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
			WHERE a.resource_id = b.resource_id
			AND a.resource_type = 'url' ORDER BY a.sort_order
		</beans:value>
	</beans:property>
	<beans:property name="sqlRolesAndMethod">
		<beans:value>
	    SELECT a.resource_pattern method, b.authority authority
			FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
			WHERE a.resource_id = b.resource_id
			AND a.resource_type = 'method' ORDER BY a.sort_order
		</beans:value>
	</beans:property>
	<beans:property name="sqlRolesAndPointcut">
		<beans:value>
			SELECT a.resource_pattern pointcut, b.authority authority
			FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
			WHERE a.resource_id = b.resource_id
			AND a.resource_type = 'pointcut' ORDER BY a.sort_order
		</beans:value>
	</beans:property>
</beans:bean>

세션사용

역할 가져오기

List<String> authorities = EgovUserDetailsHelper.getAuthorities();
 
// 1. authorites 에  권한이 있는지 체크 TRUE/FALSE
assertTrue(authorities.contains("ROLE_USER"));
assertTrue(authorities.contains("ROLE_RESTRICTED"));
assertTrue(authorities.contains("IS_AUTHENTICATED_ANONYMOUSLY"));
assertTrue(authorities.contains("IS_AUTHENTICATED_FULLY"));
assertTrue(authorities.contains("IS_AUTHENTICATED_REMEMBERED"));
 
// 2. authorites 에  ROLE 이 여러개 설정된 경우
for (Iterator<String> it = authorities.iterator(); it.hasNext();) {
	String auth = it.next();
}
 
// 3. authorites 에  ROLE 이 하나만 설정된 경우
String auth = (String) authorities.toArray()[0];

2.5 - 설정 간소화

전자정부 표준프레임워크 3.0부터 Server Security 설정을 간소화할 수 있는 기능을 제공하며, XML Schema를 통해 필요한 설정만 추가하면 된다. Security Config는 로그인, 로그아웃, 권한 관리, 동시 세션, 패스워드 해시 방식 등 다양한 보안 설정을 간단히 설정할 수 있게 해준다. 또한, Security Object Config를 통해 URL, Method, Pointcut 방식의 권한 설정과 계층적 역할 관리도 쉽게 구성할 수 있다.

설정 간소화

개요

표준프레임워크 3.0부터 Server security에 대하여 설정을 간소화 할 수 있는 방법을 제공한다. 내부적으로 필요한 설정을 가지고 있고, XML Schema를 통해 필요한 설정만을 추가할 수 있도록 제공한다.

XML namespace 및 schema 설정

설정 간소화 기능을 사용하기 위해서는 다음과 같은 xml 선언이 필요하다. 4.1 > 4.2 업그레이드 시 xsd 변경(egov-security-4.1.0.xsd > egov-security-4.2.0.xsd)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:egov-security="http://maven.egovframe.go.kr/schema/egov-security"
	xmlns:security="http://www.springframework.org/schema/security"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
		http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
		http://maven.egovframe.go.kr/schema/egov-security http://maven.egovframe.go.kr/schema/egov-security/egov-security-4.2.0.xsd">

Security Config 설정

Security에 대한 기본 설정 정보를 제공한다.

예:

<egov-security:config id="securityConfig"
	loginUrl="/uat/uia/egovLoginUsr.do"
	logoutSuccessUrl="/EgovContent.do"
	loginFailureUrl="/uat/uia/egovLoginUsr.do?login_error=1"
	accessDeniedUrl="/sec/ram/accessDenied.do"
 
	dataSource="egov.dataSource"
	jdbcUsersByUsernameQuery="SELECT USER_ID, ESNTL_ID AS PASSWORD, 1 ENABLED, USER_NM, USER_ZIP,
                                  USER_ADRES, USER_EMAIL, USER_SE, ORGNZT_ID, ESNTL_ID,
                                  (select a.ORGNZT_NM from COMTNORGNZTINFO a where a.ORGNZT_ID = m.ORGNZT_ID) ORGNZT_NM
                                  FROM COMVNUSERMASTER m WHERE CONCAT(USER_SE, USER_ID) = ?"
	jdbcAuthoritiesByUsernameQuery="SELECT A.SCRTY_DTRMN_TRGET_ID USER_ID, A.AUTHOR_CODE AUTHORITY
                                        FROM COMTNEMPLYRSCRTYESTBS A, COMVNUSERMASTER B
                                        WHERE A.SCRTY_DTRMN_TRGET_ID = B.ESNTL_ID AND B.USER_ID = ?"
	jdbcMapClass="egovframework.com.sec.security.common.EgovSessionMapping"
 
	requestMatcherType="regex"
	hash="plaintext"
	hashBase64="false"
 
	concurrentMaxSessons="1"
	concurrentExpiredUrl="/EgovContent.do"
	errorIfMaximumExceeded="false"
 
	defaultTargetUrl="/EgovContent.do"
	alwaysUseDefaultTargetUrl="true"
 
	sniff="true"
	xframeOptions="SAMEORIGIN" 
	xssProtection="true" 
	cacheControl="false"
	csrf="false"
	csrfAccessDeniedUrl="/egovCSRFAccessDenied.do"
/>

속성 설명

속성설명필수여부비고
loginUrl로그인 페이지 URL필수
logoutSuccessUrl로그아웃 처리 시 호출되는 페이지 URL필수
loginFailureUrl로그인 실패 시 호출되는 페이지 URL필수
accessDeniedUrl권한이 없는 경우 호출되는 페이지 URL필수
dataSourceDBMS 설정 dataSource선택미지정시 ‘dataSource’ bean name 사용
jdbcUsersByUsernameQuery인증에 사용되는 query선택default : “select user_id, password, enabled, users.* from users where user_id = ?”
jdbcAuthoritiesByUsernameQuery인증된 사용자의 권한(authority) 조회 query선택default : “select user_id, authority from authorites where user_id = ?”
jdbcMapClass사용자 정보 mapping 처리 class선택default : egovframework.rte.fdl.security.userdetails.DefaultMapUserDetailsMapping
requestMatcherType패턴 매칭 방식(regex, ant, ciRegex: case-insensitive regex)선택default : regex
hash패스워드 저장 방식 (sha-256, plaintext, sha, md5, bcrypt)선택default : sha-256
hashBase64hash값 base64 인코딩 사용 여부선택default : true
concurrentMaxSessons동시 접속가능 연결 수선택default : 999
concurrentExpiredUrlexpired된 경우 redirect되는 페이지 URL선택
errorIfMaximumExceeded중복 로그인 방지 옵션필수default : false
defaultTargetUrl로그인 성공시 redirect되는 페이지 URL선택미지정시 처음 접속하고자 했던 페이지 URL로 redirect됨
alwaysUseDefaultTargetUrl로그인 이후 설정한 페이지로 이동하게 하는 옵션필수default : true
sniff선언된 콘텐츠 유형으로부터 벗어난 응답에 대한 브라우저의 MIME 가로채기를 방지 여부필수default : true
xframeOptionssniff 옵션 이 ture 일때 X-Frame-Options 범위설정선택DENY, SAMEORIGIN
xssProtectionXSS Protection 기능의 사용 여부필수default : true
cacheControl캐쉬 비활성화 여부 옵션필수default : false
csrfspring security의 csrf 기능 사용 여부필수default : false
csrfAccessDeniedUrl토큰 검증이 실패했을 경우 호출되는 페이지 URL필수
useExpressionsSpring 표현 언어(SpEL) 설정 옵션선택default : false
  • DefaultMapUserDetailsMapping : VO 없이 Map 방식으로 매핑을 처리함 (jdbcUsersByUsernameQuery 상에 지정된 컬럼에 대하여 camel case 방식으로 Hash Map 키 생성 처리)
  • useExpressions 속성은 사용할 경우 설정 추가 필요

Security Config Initializer 설정

Security에 대한 초기화 처리 정보를 제공한다.

예:

<egov-security:initializer
	id="initializer"
	supportPointcut="true"
/>

속성 설명

속성설명필수여부비고
supportPointcutpointcut 방식 지원 여부선택default : false
supportMethodmethod 방식 지원 여부선택default : true

Security Object Config 설정

Security에 대한 기본 query 설정 정보를 제공한다.

예:

<egov-security:secured-object-config
	id="securedObjectConfig"
	roleHierarchyString="
			ROLE_ADMIN > ROLE_USER
			ROLE_USER > ROLE_RESTRICTED
			ROLE_RESTRICTED > IS_AUTHENTICATED_FULLY
			IS_AUTHENTICATED_FULLY >	IS_AUTHENTICATED_REMEMBERED
			IS_AUTHENTICATED_REMEMBERED > IS_AUTHENTICATED_ANONYMOUSLY"
	sqlRolesAndUrl="
			SELECT auth.URL url, code.CODE_NM authority
			FROM RTETNAUTH auth, RTETCCODE code
			WHERE code.CODE_ID = auth.MNGR_SE"
/>

속성 설명

속성설명필수여부비고
roleHierarchyString계층처리를 위한 설정 문자열 지정선택미지정시 DB로부터 지정된 설정정보 지정
sqlRolesAndUrlURL 방식 role 지정 query선택미지정시 SecuredObjectDAO의 기본 query가 처리됨
sqlRolesAndMethodmethod 방식 role 지정 query선택
sqlRolesAndPointcutpointcut 방식 role 지정 query선택
sqlRegexMatchedRequestMappingrequest 마다 best matching url 보호자원 지정 query선택
sqlHierarchicalRoles계층처리를 위한 query선택

2.6 - Server Security 업그레이드

표준프레임워크 2.7에서 3.0으로 Server Security 업그레이드 시, Spring Security 패키지와 API 변경, dependencies 수정, web.xml 수정 등이 필요하다. 주요 API 변경 사항에는 SpringSecurityException 삭제, ConfigAttributeDefinition 변경, SavedRequest 인터페이스화 등이 있으며, 패키지 경로 변경과 GrantedAuthority 방식의 변경도 적용해야 한다. 인증 및 GET 방식 호출 방지 처리, Mapping 클래스 변경, 불필요한 클래스 삭제 등 여러 단계에서 소스 코드와 설정의 수정을 요구한다.

Server Security 업그레이드

개요

표준프레임워크 2.7(Spring Security 2.0.4)에서 3.0(Spring Security 3.2.3)로 업그레이드 Server security의 경우 설정 변경뿐만 아니라 소스 상의 변경 작업이 필요하다.

주요 변경내용 (Spring Security 부분)

dependencies 및 패키지 변경

  • spring-security-core (org.springframework.security.core, org.springframework.security.access, etc.)
  • spring-security-web (org.springframework.security.web)
  • spring-security-config (org.springframework.security.config)

API 변경

  • SpringSecurityException 삭제
  • ConfigAttributeDefinition ⇒ Collection
  • SavedRequest : class ⇒ interface (DefaultSavedRequest 대체)

기타

  • 다중 http elements 지원
  • stateless 인증 지원
  • DebugFilter 추가 (debugging용)
  • hasPermission 표현식 지원 (authorize JSP tag)
  • 등등

실행환경 부분 업그레이드 절차

1. dependency 수정

<dependency>
    <groupId>egovframework.rte</groupId>
    <artifactId>egovframework.rte.fdl.security</artifactId>
    <version>3.0.0</version>
</dependency>
  • 적용 버전은 최신 버전 확인 후 적용(patch 버전 등)

2. web.xml 수정

접속 제한을 사용하는 경우 web.xml 상에 HttpSessionEventPublisher listener의 패키지 변경 필요

  • 기존 :
<listener>
	<listener-class>org.springframework.security.ui.session.HttpSessionEventPublisher</listener-class>
</listener>
  • 변경 :
<listener>
	<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>

3. Spring Security 설정 수정

다음 설정을 참조하여 관련 설정을 변경한다. (Spring Security쪽 패키지 등)

<beans:beans  xmlns="http://www.springframework.org/schema/security"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
						http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd
						http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.2.xsd">
 
	<beans:bean id="securedObjectService" class="egovframework.rte.fdl.security.securedobject.impl.SecuredObjectServiceImpl">
		<beans:property name="securedObjectDAO" ref="securedObjectDAO"/>
		<beans:property name="requestMatcherType" value="regex"/>	<!--  default : ant -->
	</beans:bean>
 
	<beans:bean id="securedObjectDAO" class="egovframework.rte.fdl.security.securedobject.impl.SecuredObjectDAO" >
		<beans:property name="dataSource" ref="dataSource"/>
		<!--
		<beans:property name="sqlHierarchicalRoles">
			<beans:value>
				SELECT a.child_role child, a.parent_role parent
				FROM ROLES_HIERARCHY a LEFT JOIN ROLES_HIERARCHY b on (a.child_role = b.parent_role)
			</beans:value>
		</beans:property>
		<beans:property name="sqlRolesAndUrl">
			<beans:value>
				SELECT a.resource_pattern url, b.authority authority
				FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
				WHERE a.resource_id = b.resource_id
				AND a.resource_type = 'url' ORDER BY a.sort_order
			</beans:value>
		</beans:property>
		<beans:property name="sqlRolesAndMethod">
			<beans:value>
		    SELECT a.resource_pattern method, b.authority authority
				FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
				WHERE a.resource_id = b.resource_id
				AND a.resource_type = 'method' ORDER BY a.sort_order
			</beans:value>
		</beans:property>
		<beans:property name="sqlRolesAndPointcut">
			<beans:value>
				SELECT a.resource_pattern pointcut, b.authority authority
				FROM SECURED_RESOURCES a, SECURED_RESOURCES_ROLE b
				WHERE a.resource_id = b.resource_id
				AND a.resource_type = 'pointcut' ORDER BY a.sort_order
			</beans:value>
		</beans:property>
		-->
	</beans:bean>
 
	<!-- 불필요 삭제 -->
	<!--   
	<beans:bean id="userDetailsServiceWrapper" class="org.springframework.security.userdetails.hierarchicalroles.UserDetailsServiceWrapper">
		<beans:property name="roleHierarchy" ref="roleHierarchy"/>
		<beans:property name="userDetailsService" ref="jdbcUserService"/>
	</beans:bean>
	-->
 
	<beans:bean id="roleHierarchy" class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl" >
		<!-- XML 사용 
		<beans:property name="hierarchy">
			<beans:value>
				ROLE_ADMIN > ROLE_USER
				ROLE_USER > ROLE_RESTRICTED
				ROLE_RESTRICTED > IS_AUTHENTICATED_FULLY
				IS_AUTHENTICATED_REMEMBERED > IS_AUTHENTICATED_ANONYMOUSLY
			</beans:value>
		</beans:property>
		-->
		<!-- DB 사용 -->
		<beans:property name="hierarchy" ref="hierarchyStrings"/>
	</beans:bean>
 
	<beans:bean id="hierarchyStrings" class="egovframework.rte.fdl.security.userdetails.hierarchicalroles.HierarchyStringsFactoryBean" init-method="init">
		<beans:property name="securedObjectService" ref="securedObjectService"/>
	</beans:bean>
 
	<!-- 
	Access Decision Manager는 자동으로 생성되기 때문에 선언 불필요 
	bean id : org.springframework.security.access.vote.AffirmativeBased#0
	※ #0 부분은 숫자 부분은 선언 순으로 순차적으로 생성됨
	-->
	<!--
	<beans:bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
		<beans:property name="allowIfAllAbstainDecisions" value="false" />
		<beans:property name="decisionVoters">
			<beans:list>
				<beans:bean class="org.springframework.security.access.vote.RoleVoter">
					<beans:property name="rolePrefix" value="ROLE_" />
				</beans:bean>
				<beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter" />
			</beans:list>
		</beans:property>
	</beans:bean>
	-->
 
	<beans:bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">	
		<beans:property name="authenticationManager" ref="org.springframework.security.authenticationManager" />
		<beans:property name="accessDecisionManager" ref="org.springframework.security.access.vote.AffirmativeBased#0" />
		<beans:property name="securityMetadataSource" ref="databaseSecurityMetadataSource" />
	</beans:bean>
 
	<beans:bean id="databaseSecurityMetadataSource" class="egovframework.rte.fdl.security.intercept.EgovReloadableFilterInvocationSecurityMetadataSource">
		<beans:constructor-arg ref="requestMap" />	
		<beans:property name="securedObjectService" ref="securedObjectService"/>
	</beans:bean>
 
	<!--  url  -->
	<beans:bean id="requestMap" 	class="egovframework.rte.fdl.security.intercept.UrlResourcesMapFactoryBean" init-method="init">
		<beans:property name="securedObjectService" ref="securedObjectService"/>
	</beans:bean>
 
	<!-- 지정 불필요 : request-matcher 참조 -->
	<!-- 
	<beans:bean id="regexUrlPathMatcher" class="org.springframework.security.web.util.matcher.RegexRequestMatcher" />	
 	-->
 
 	<http pattern="/css/**" security="none"/>    
    <http pattern="/images/**" security="none"/>
 	<http pattern="/js/**" security="none"/>
 	<http pattern="\A/WEB-INF/jsp/.*\Z" request-matcher="regex" security="none"/> 	
 
	<http access-denied-page="/system/accessDenied.do" request-matcher="regex">
		<form-login login-processing-url="/j_spring_security_check"
					authentication-failure-url="/cvpl/EgovCvplLogin.do?login_error=1"
					default-target-url="/index.jsp?flag=L"
					login-page="/cvpl/EgovCvplLogin.do" />
		<anonymous/>
		<logout logout-success-url="/cvpl/EgovCvplLogin.do"/>
 
		<!-- for authorization -->
		<custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="filterSecurityInterceptor"/>
	</http>
 
	<!--
	authentication-manager 기본 생성 bean id :  org.springframework.security.authenticationManager
		(alias로 변경할 수 있음)
	-->
	<authentication-manager>
		<authentication-provider user-service-ref="jdbcUserService">
			<password-encoder  hash="sha-256" base64="true"/>
		</authentication-provider>		
	</authentication-manager>	
 
	<!-- userDetailsServiceWrapper -->
	<!-- customizing user table, authorities table -->
 
	<!--<jdbc-user-service id="jdbcUserService" data-source-ref="dataSource"
		users-by-username-query="SELECT USER_ID,PASSWORD,ENABLED,BIRTH_DAY FROM USERS WHERE USER_ID = ?"
		authorities-by-username-query="SELECT USER_ID,AUTHORITY FROM AUTHORITIES WHERE USER_ID = ?"/>-->
 
	<beans:bean id="jdbcUserService" class="egovframework.rte.fdl.security.userdetails.jdbc.EgovJdbcUserDetailsManager" >
		<beans:property name="usersByUsernameQuery" value="SELECT USER_ID,PASSWORD,ENABLED,USER_NAME,BIRTH_DAY,SSN FROM USERS WHERE USER_ID = ?"/>
		<beans:property name="authoritiesByUsernameQuery" value="SELECT USER_ID,AUTHORITY FROM AUTHORITIES WHERE USER_ID = ?"/>
		<beans:property name="roleHierarchy" ref="roleHierarchy"/>
		<beans:property name="dataSource" ref="dataSource"/>
		<beans:property name="mapClass" value="egovframework.rte.fdl.security.userdetails.EgovUserDetailsMapping"/>
	</beans:bean>
 
	<!-- method -->
	<beans:bean id="methodSecurityMetadataSourceAdvisor" class="org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor">
		<beans:constructor-arg value="methodSecurityInterceptor" />
		<beans:constructor-arg ref="delegatingMethodSecurityMetadataSource" />
		<beans:constructor-arg value="delegatingMethodSecurityMetadataSource" />
	</beans:bean>
 
	<beans:bean id="methodSecurityInterceptor" class="org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor">
		<beans:property name="validateConfigAttributes" value="false" />
		<beans:property name="authenticationManager" ref="org.springframework.security.authenticationManager"/>
		<beans:property name="accessDecisionManager" ref="org.springframework.security.access.vote.AffirmativeBased#0"/>
		<beans:property name="securityMetadataSource" ref="delegatingMethodSecurityMetadataSource" />
	</beans:bean>
 
    <beans:bean id="delegatingMethodSecurityMetadataSource" class="org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource">
        <beans:constructor-arg>
            <beans:list>
                <beans:ref bean="methodSecurityMetadataSources" />
                <beans:bean class="org.springframework.security.access.annotation.SecuredAnnotationSecurityMetadataSource" />
                <beans:bean class="org.springframework.security.access.annotation.Jsr250MethodSecurityMetadataSource" />
            </beans:list>
        </beans:constructor-arg>
    </beans:bean>
 
	<beans:bean id="methodSecurityMetadataSources" class="org.springframework.security.access.method.MapBasedMethodSecurityMetadataSource">
		<beans:constructor-arg ref="methodMap" />
	</beans:bean>
 
	<beans:bean id="methodMap" class="egovframework.rte.fdl.security.intercept.MethodResourcesMapFactoryBean" init-method="init">
		<beans:property name="securedObjectService" ref="securedObjectService"/>
		<beans:property name="resourceType" value="method"/>
	</beans:bean>
 
	<!-- pointcut -->
	<!-- if no map, there is a error that "this map must not be empty; it must contain at least one entry" -->
	<!-- // so there is dummy entry
	<beans:bean id="protectPointcutPostProcessor" class="org.springframework.security.config.method.ProtectPointcutPostProcessor">
		<beans:constructor-arg ref="methodSecurityMetadataSources" />
		<beans:property name="pointcutMap" ref="pointcutMap"/>
	</beans:bean>
 
	<beans:bean id="pointcutMap" class="egovframework.rte.fdl.security.intercept.MethodResourcesMapFactoryBean" init-method="init">
		<beans:property name="securedObjectService" ref="securedObjectService"/>
		<beans:property name="resourceType" value="pointcut"/>
	</beans:bean>
	-->
</beans:beans>

4. 불필요 class 삭제

자체 적용된 server security에 대한 소스 삭제 정리 Ex:

  • org.springframework.security.intercept.web.EgovReloadableDefaultFilterInvocationDefinitionSource : 패키지 변경으로 인하여 불필요 (egovframework.rte.fdl.security.intercept.EgovReloadableFilterInvocationSecurityMetadataSource)
  • …security.intercept.*, …security.securedobject.securedobject.*, …security.securedobject.userdetails.* 등 : 업그레이된 실행환경쪽 패키지(egovframework.rte.fdl.security.*)를 참조하기 때문에 불필요

5. Mapping 클래스 mapRow 메소드 변경

jdbcUserService(egovframework.rte.fdl.security.userdetails.jdbc.EgovJdbcUserDetailsManager)에 의해 지정된 mapClass는 EgovUsersByUsernameMapping 클래스를 extend 하도록 되어 있는데, 해당 EgovUsersByUsernameMapping의 mapRow() 메소드의 return 타입이 Object에서 EgovUserDtails로 변경되었다.

  • 기존 :
public class EgovSessionMapping extends EgovUsersByUsernameMapping {
    ...
 
    @Override
    protected Object mapRow(ResultSet rs, int rownum) throws SQLException {
        ...
    }
}
  • 변경 :
public class EgovSessionMapping extends EgovUsersByUsernameMapping {
    ...
 
    @Override
    protected EgovUserDetails mapRow(ResultSet rs, int rownum) throws SQLException {
        ...
    }
}

6. 참조 패키지 및 클래스 변경

Spring security 관련 패키지 변경 등에 따라 일부 참조 클래스에 대한 패키지 변경 필요

  • org.springframework.security.Authentication → org.springframework.security.core.Authentication
  • org.springframework.security.GrantedAuthority → org.springframework.security.core.GrantedAuthority
  • org.springframework.security.context.SecurityContext → org.springframework.security.core.context.SecurityContext
  • org.springframework.security.context.SecurityContextHolder → org.springframework.security.core.context.SecurityContextHolder
  • 등등

7. GrantedAuthority 방식 변경 (array -> collection 타입)

GrantedAuthority[] → Collection<GrantedAuthority> 변경 적용

  • 이전 코드 :
GrantedAuthority[] authorities = authentication.getAuthorities();
 
for (int i = 0; i < authorities.length; i++) {
	listAuth.add(authorities[i].getAuthority());
}
  • 변경 코드 :
Collection<GrantedAuthority> authorities = (Collection<GrantedAuthority>) authentication.getAuthorities();
 
for (GrantedAuthority authority : authorities) {
	listAuth.add(authority.getAuthority());
}

8. SecurityContext의 getAuthentication() 방식 변경

기존의 경우 로그인되지 않은 경우 SecurityContext의 getAuthentication()의 리턴 값이 null이었으나,

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
if (EgovObjectUtil.isNull(authentication)) {
	return null;
}

신규 버전의 경우는 null이 아닌 값으로 처리된다.

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
 
if (EgovObjectUtil.isNull(authentication)) {
	log.debug("## authentication object is null!!");
	return null;
}
 
if (authentication.getPrincipal() instanceof EgovUserDetails) {
	EgovUserDetails details = (EgovUserDetails) authentication.getPrincipal();
 
 
	log.debug("## EgovUserDetailsHelper.getAuthenticatedUser : AuthenticatedUser is {}", details.getUsername());
 
	return details.getEgovUserVO();
} else {
	return authentication.getPrincipal();
}

따라서 사용자 정보를 취득하는 부분은 자체 적용 부분이 아닌 실행환경 제공 부분(egovframework.rte.fdl.security.userdetails.util.EgovUserDetailsHelper)을 사용한다.

9. GET 방식 인증 불가 처리

신규 버전의 경우는 GET 방식으로 j_spring_security_check를 호출할 수 없게 되었다.(j_spring_security_check URL을 내부적으로 redirect 호출하는 경우에만 해당) 오류 메시지 : Authentication request failed: org.springframework.security.authentication.AuthenticationServiceException: Authentication method not supported: GET

이 경우 다음 코드와 같이 GET 방식이 아닌 filter chain 호출 방식으로 변경해주어야 한다

  • 기존 코드 :
return "redirect:/j_spring_security_check?j_username=" + resultVO.getUserSe() + resultVO.getId() + "&j_password=" + resultVO.getUniqId();
  • 변경 코드 (일반 Controller인 경우) :
@RequestMapping(value="/uat/uia/actionSecurityLogin.do")
public String actionSecurityLogin(@ModelAttribute("loginVO") LoginVO loginVO, 
	HttpServletRequest request, HttpServletResponse response,
	ModelMap model)
		throws Exception {
	...
 
	UsernamePasswordAuthenticationFilter springSecurity = null;
 
	ApplicationContext act = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getSession().getServletContext());
	@SuppressWarnings("rawtypes")
	Map beans = act.getBeansOfType(UsernamePasswordAuthenticationFilter.class);
	if (beans.size() > 0) {
		springSecurity = (UsernamePasswordAuthenticationFilter)beans.values().toArray()[0];
	} else {
		throw new IllegalStateException("No AuthenticationProcessingFilter");
	}
 
	springSecurity.setContinueChainBeforeSuccessfulAuthentication(false);	// false 이면 chain 처리 되지 않음.. (filter가 아닌 경우 false로...)
 
	springSecurity.doFilter(
		new RequestWrapperForSecurity(request, resultVO.getUserSe() + resultVO.getId() , resultVO.getUniqId()), 
		response, null);
 
	return "forward:/cmm/main/mainPage.do";	// 성공 시 페이지.. (redirect 불가)
 
	...
}
 
...
class RequestWrapperForSecurity extends HttpServletRequestWrapper {	
	private String username = null;
	private String password = null;
 
	public RequestWrapperForSecurity(HttpServletRequest request, String username, String password) {
		super(request);
 
		this.username = username;
		this.password = password;
	}
 
	@Override
	public String getRequestURI() {
		return ((HttpServletRequest)super.getRequest()).getContextPath() + "/j_spring_security_check";
	}
 
	@Override
	public String getParameter(String name) {
        if (name.equals("j_username")) {
        	return username;
        }
 
        if (name.equals("j_password")) {
        	return password;
        }
 
        return super.getParameter(name);
    }
}
  • 변경 코드 (Filter인 경우) :
public class EgovSpringSecurityLoginFilter implements Filter{
	private FilterConfig config;
 
	public void init(FilterConfig filterConfig) throws ServletException {
		this.config = filterConfig;
	}
 
 
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		...
 
		HttpServletRequest httpRequest = (HttpServletRequest)request;
		HttpServletResponse httpResponse = (HttpServletResponse)response;
 
		...
 
		UsernamePasswordAuthenticationFilter springSecurity = null;
 
		ApplicationContext act = WebApplicationContextUtils.getRequiredWebApplicationContext(config.getServletContext());
		@SuppressWarnings("rawtypes")
		Map beans = act.getBeansOfType(UsernamePasswordAuthenticationFilter.class);
		if (beans.size() > 0) {
			springSecurity = (UsernamePasswordAuthenticationFilter)beans.values().toArray()[0];
		} else {
			throw new IllegalStateException("No AuthenticationProcessingFilter");
		}
 
		//springSecurity.setContinueChainBeforeSuccessfulAuthentication(false);	// false 이면 chain 처리 되지 않음.. (filter가 아닌 경우 false로...)
 
		springSecurity.doFilter(
			new RequestWrapperForSecurity(request, resultVO.getUserSe() + resultVO.getId() , resultVO.getUniqId()), 
			response, chain);
 
		...
	}
 
	...
}
 
class RequestWrapperForSecurity extends HttpServletRequestWrapper {	
	private String username = null;
	private String password = null;
 
	public RequestWrapperForSecurity(HttpServletRequest request, String username, String password) {
		super(request);
 
		this.username = username;
		this.password = password;
	}
 
	@Override
	public String getRequestURI() {
		return ((HttpServletRequest)super.getRequest()).getContextPath() + "/j_spring_security_check";
	}
 
	@Override
	public String getParameter(String name) {
        if (name.equals("j_username")) {
        	return username;
        }
 
        if (name.equals("j_password")) {
        	return password;
        }
 
        return super.getParameter(name);
    }
}

2.7 - Session 방식 접근제어 권한설정

표준프레임워크 3.9부터 Session 방식의 접근제어 권한관리를 설정할 수 있으며, 이를 위해 XML 선언 및 SQL 쿼리를 포함한 기본 설정이 필요하다. 롤 권한 변경 시 서버 재기동 없이 AuthorityResourceMetadatareload() 메소드를 호출하여 설정을 적용할 수 있다.

Session 방식 접근제어 권한설정

개요

표준프레임워크 3.9부터 Session 방식으로 접근제어 권한관리를 설정 할 수 있는 방법을 제공한다. 내부적으로 필요한 설정을 가지고 있고, XML Schema를 통해 필요한 설정만을 추가할 수 있도록 제공한다. 이 기능을 사용하기 위해서는 globals.properties 파일에서 Globals.Auth = session 로 설정한다.

환경설정

pom.xml (dependency추가)

Session 방식의 접근제어 권한관리를 사용하기 위해서는 표준프레임워크 실행환경 구성요소중 org.egovframe.rte.fdl.access 라이브러리가 설치되어야 한다.

<dependency>
	<groupId>org.egovframe.rte</groupId>
	<artifactId>org.egovframe.rte.fdl.access</artifactId>
	<version>${org.egovframe.rte.version}</version>
</dependency>

XML namespace 및 schema 설정

접근제어를 설정하기 위해서는 다음과 같은 xml 선언이 필요하다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:egov-access="http://maven.egovframe.go.kr/schema/egov-access"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://maven.egovframe.go.kr/schema/egov-access http://maven.egovframe.go.kr/schema/egov-access/egov-access-4.2.0.xsd">

Session 접근제어 설정

Session 방식 접근제어 권한관리에 대한 기본 설정 정보를 제공한다.

<egov-access:config id="egovAccessConfig"
	globalAuthen="session"
	mappingPath="/**/*.do"
	dataSource="egov.dataSource"
	loginUrl="/uat/uia/egovLoginUsr.do"
	accessDeniedUrl="/uat/uia/egovLoginUsr.do?auth_error=1"
	sqlAuthorityUser="SELECT CONCAT(B.USER_SE, B.USER_ID) USERID, A.AUTHOR_CODE AUTHORITY
		FROM COMTNEMPLYRSCRTYESTBS A, COMVNUSERMASTER B
		WHERE A.SCRTY_DTRMN_TRGET_ID = B.ESNTL_ID"
	sqlRoleAndUrl="SELECT A.ROLE_PTTRN URL, B.AUTHOR_CODE AUTHORITY
		FROM COMTNROLEINFO A, COMTNAUTHORROLERELATE B
		WHERE A.ROLE_CODE = B.ROLE_CODE
		AND A.ROLE_TY = 'url'
		ORDER BY A.ROLE_SORT"
	requestMatcherType="regex"
	excludeList="/uat/uia/**, /index.do, /EgovLeft.do, /EgovContent.do, /EgovTop.do, /EgovBottom.do, /validator.do, /uss/umt/**, /sec/rnc/EgovRlnmCnfirm.do, /EgovModal.do"
/>

속성 설명

속성설명필수여부비고
globalAuthenglobals.properties 설정과 동일하게 적용 (Globals.Auth = session 설정 사용시 globalAuthen = “session”으로 값을 동일하게 일치하여 설정 필요)필수
dataSourceDBMS 설정 dataSource필수
loginUrl로그인 페이지 URL필수
accessDeniedUrl권한이 없는 경우 호출되는 페이지 URL필수
sqlAuthorityUser인증된 사용자의 권한(authority) 조회 query필수
sqlRoleAndUrlRole 및 URL 패턴필수
requestMatcherType패턴 매칭 방식(regex, ant, ciRegex: case-insensitive regex)필수default : regex
excludeList접근제한 예외처리 URL(구분자: ,)필수
  • excludeList(접근제한 예외 목록 URL) 예시 값
    • 회원관리 : /uat/uia/**
    • 실명확인 : /sec/rnc/**
    • 우편번호 : /sym/ccm/zip/**
    • 로그인이미지관리 : /uss/ion/lsi/**
    • 약관확인 : /uss/umt/**
    • 포털예제배너 : /uss/ion/bnr/getBannerImage.do
    • 처음화면 : /index.do
    • 로그인화면이미지 : /cmm/fms/getImage.do
    • 좌측메뉴 : /EgovLeft.do
    • 초기화면 : /EgovContent.do
    • 상단메뉴 : /EgovTop.do
    • 하단메뉴 : /EgovBottom.do
    • 모달팝업 : /EgovModal.do
    • 만족도조사 : /cop/stf/selectSatisfactionList.do
    • 만족도조사 선택 : /cop/stf/selectSingleSatisfaction.do
    • 댓글 : /cop/cmt/selectArticleCommentList.do
    • 댓글 선택 : /cop/cmt/updateArticleCommentView.do
  • 동적 include 방식 - JSP 에서 <c:import> 혹은 <jsp:include> 사용시 호출대상 URL이 .do 인 경우
    • 호출대상.do 뿐만 아니라 .do URL이 호출하는 JSP파일도 권한관리에 등록해야 함

Session 접근제어 재설정

Session 접근제어에서 사용자의 롤권한변경 후 서버 재기동 없이 적용하는 방법을 제공한다.

import org.egovframe.rte.fdl.access.bean.AuthorityResourceMetadata;
 
@Resource(name="authorityResource")
private AuthorityResourceMetadata sessionResourceMetadata;
 
@RequestMapping(value="/insertAuthorGroupInsert.do")
public String insertAuthorGroup() {
...
    sessionResourceMetadata.reload();
...
}

2.8 - Scheduling 서비스

Scheduling 서비스는 주기적 작업을 관리하는 기능으로, Quartz 스케줄러와 Spring을 통합하여 사용된다. Quartz는 작업(Job), 스케줄(Trigger)을 분리해 유연성을 제공하며, Spring에서 JobDetailBean과 MethodInvokingJobDetailFactoryBean을 통해 작업을 생성하고, SimpleTriggerBean과 CronTriggerBean으로 작업을 스케줄링한다.

Scheduling 서비스

개요

Scheduling 서비스는 어플리케이션 서버 내에서 주기적으로 발생하거나 반복적으로 발생하는 작업을 지원하는 기능으로서 유닉스의 크론(Cron) 명령어와 유사한 기능을 제공한다.
실행환경 Scheduling 서비스는 오픈소스 소프트웨어로 Quartz 스케쥴러를 사용한다. 본 장에서는 Quartz 스케쥴러의 기본 개념을 살펴본 후, IoC 서비스를 제공하는 Spring과 Quartz 스케쥴러를 통합하여 사용하는 방법을 살펴본다.

설명

Quartz 스케쥴러

Quartz 스케쥴러 실행과 관계된 주요 요소는 Scheduler, Job, JobDetail, Trigger 가 있다.

  • Scheduler 는 Quartz 실행 환경을 관리하는 핵심 개체이다.
  • Job 은 사용자가 수행할 작업을 정의하는 인터페이스로서 Trigger 개체를 이용하여 스케쥴할 수 있다.
  • JobDetail 는 작업명과 작업그룹과 같은 수행할 Job에 대한 상세 정보를 정의하는 개체이다.
  • Trigger 는 정의한 Job 개체의 실행 스케쥴을 정의하는 개체로서 Scheduler 개체에게 Job 수행시점을 알려주는 개체이다.

Quartz 스케쥴러는 수행 작업을 정의하는 Job과 실행 스케쥴을 정의하는 Trigger를 분리함으로써 유연성을 제공한다. Job 과 실행 스케쥴을 정의한 경우, Job은 그대로 두고 실행 스케쥴만을 변경할 수 있다. 또한 하나의 Job에 여러 개의 실행 스케쥴을 정의할 수 있다.

Quartz 스케쥴러 사용 예제

Quartz 스케쥴러의 이해를 돕기 위해 간단한 예제를 살펴본다. 다음 예는 Quartz 매뉴얼에서 참조한 것으로 Quartz를 사용하는 방법과 사용자 Job을 설정하는 방법을 보여준다.

사용자 정의 Job

사용자는 Job 개체를 생성하기 위해 org.quartz.Job 인터페이스를 구현하고 심각한 오류가 발생한 경우 JobExecutionException 예외를 던질 수 있다. Job 인터페이스는 단일 메소드로 execute()을 정의한다.

public class DumbJob implements Job {
	public void execute(JobExecutionContext context) throws JobExecutionException {
		System.out.println("DumbJob is executing.");
	}
}
  • DumbJob은 Job 인터페이스의 execute() 메소드를 구현한다.
  • execute() 메소드는 단순히 Job이 수행됨을 표시하는 메시지를 출력한다.
Quartz 사용 코드
  JobDetail jobDetail = 
            new JobDetail("myJob",// Job 명
              sched.DEFAULT_GROUP,  // Job 그룹명('null' 값인 경우 DEFAULT_GROUP 으로 정의됨)
              DumbJob.class);       // 실행할 Job 클래스

  Trigger trigger = TriggerUtils.makeDailyTrigger(8, 30);  // 매일 08시 30분 실행
  trigger.setStartTime(new Date()); // 즉시 시작
  trigger.setName("myTrigger");

  sched.scheduleJob(jobDetail, trigger);
  • 우선 Job 설정을 위해 JobDetail 클래스를 정의한다.
  • TriggerUtils을 이용하여 매일 8시30분 실행하는 Trigger를 생성한다.
  • 마지막으로, Scheduler에 JobDetail과 Trigger를 등록한다.

Spring 과 Quartz 통합

Spring은 Scheduling 지원을 위한 통합 클래스를 제공한다. Spring Framework는 JDK 1.3 버전부터 포함된 Timer 와 오픈소스 소프트웨어인 Quartz 스케쥴러를 지원한다. 여기서는 Quartz 스케쥴러와 Spring을 통합하여 사용하는 방법을 살펴본다.
Quartz 스케쥴러와의 통합을 위해 Spring은 Spring 컨텍스트 내에서 Quart Scheduler와 JobDetail, Trigger 를 빈으로 설정할 수 있도록 지원한다. 다음은 예제를 중심으로 Quartz 작업 생성과 작업 스케쥴링, 작업 시작 방법을 살펴본다.

작업 생성

Spring은 작업 생성을 위한 방법으로 다음 두 가지 방식을 제공한다.

  • JobDetailBean을 이용한 방법으로, QuartzJobBean을 상속받아 Job 클래스를 생성하는 방법
  • MethodInvokingJobDetailFactoryBean을 이용하여 Bean 객체의 메소드를 직접 호출하는 방법
JobDetailBean을 이용한 작업 생성

JobDetail는 작업 실행에 필요한 정보를 담고 있는 객체이다. Spring은 JobDetail 빈 생성을 위해 JobDetailBean을 제공한다. 예를 들면 다음과 같다.

JobDetailBean 소스 코드
package egovframework.rte.fdl.scheduling.sample;

public class SayHelloJob extends QuartzJobBean {

	private String name;

	public void setName (String name) {
		this.name = name;	
	}

	@Override
	protected void executeInternal (JobExecutionContext ctx) throws JobExecutionException {
		System.out.println("Hello, " + name);
	}
}
  • SayHelloJob 클래스는 작업 생성을 위해 QuartzJobBean의 executeInternal(..) 함수를 오버라이드한다.
JobDetailBean 설정
<bean id="jobDetailBean"
	class="org.springframework.scheduling.quartz.JobDetailBean">
	<property name="jobClass" value="egovframework.rte.fdl.scheduling.sample.SayHelloJob" />
	<property name="jobDataAsMap">
		<map>
			<entry key="name" value="JobDetail"/>
		</map>
	</property>
</bean>
  • jobDataAsMap 개체를 이용하여 JobDetail 개체에 Job 설정에 필요한 속성 정보를 전달한다.
MethodInvokingJobDetailFactoryBean을 이용한 작업 생성
소스 코드
package egovframework.rte.fdl.scheduling.sample;

public class SayHelloService {

	private String name;

	public void setName (String name) {
		this.name = name;	
	}

	public void sayHello () {
		System.out.println("Hello, " + this.name);
	}
}
  • 작업 수행을 할 Bean 클래스를 정의한다.
설정
<bean id="sayHelloService" class="egovframework.rte.fdl.scheduling.sample.SayHelloService">
	<property name="name" value="FactoryBean"/>
</bean>

<bean id="jobDetailFactoryBean" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
	<property name="targetObject" ref="sayHelloService" />
	<property name="targetMethod" value="sayHello" />
	<property name="concurrent" value="false" />
</bean>
  • 정의한 Bean 객체의 메소드를 직접 호출하는 작업을 생성하기 위해 MethodInvokingJobDetailFactoryBean을 정의한다.

작업 스케쥴링

Spring에서 주로 사용되는 Trigger타입은 SimpleTriggerBean과 CronTriggerBean 이 있다. SimpleTrigger 는 특정 시간, 반복 회수, 대기 시간과 같은 단순 스케쥴링에 사용된다. CronTrigger 는 유닉스의 Cron 명령어와 유사하며, 복잡한 스케쥴링에 사용된다. CronTrigger 는 달력을 이용하듯 특정 시간, 요일, 월에 Job 을 수행하도록 설정할 수 있다. 다음은 SimpleTriggerBean과 CronTriggerBean을 이용하여 앞서 생성한 작업을 스케쥴링하는 방법을 살펴본다.

SimpleTriggerBean을 이용한 설정
<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
	<property name="jobDetail" ref="jobDetailBean" />
        <!-- 즉시 시작 -->
	<property name="startDelay" value="0" />
   	<!-- 매 10초마다 실행 -->
	<property name="repeatInterval" value="10000" />
</bean>
  • 앞서 JobDetailBean 을 이용하여 생성한 작업을 스케쥴링을 위한 Trigger 에 등록한다. SimpleTriggerBean은 즉시 시작하고 매 10초마다 실행하도록 설정하였다.
CronTriggerBean을 이용한 설정
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
   	<property name="jobDetail" ref="jobDetailFactoryBean" />
   	<!-- 매 10초마다 실행 -->
   	<property name="cronExpression" value="*/10 * * * * ?" />
</bean>
  • 앞서 MethodInvokingJobDetailFactoryBean 을 이용하여 생성한 작업을 스케쥴링을 위한 Trigger 에 등록한다. CronTriggerBean은 매 10초마다 실행하도록 설정하였다. 크론 표현식에 대한 자세한 설명은 Quartz Cron 표현식를 참조한다.

작업 시작하기

스케쥴링한 작업의 시작을 위해 Spring 은 SchedulerFactoryBean을 제공한다.

설정
<bean id="scheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
	<property name="triggers">
		<list>
			<ref bean="simpleTrigger" />
			<ref bean="cronTrigger" />
		</list>
	</property>
</bean>
  • SchedulerFactoryBean 을 이용하여 SimpleTriggerBean 과 CronTriggerBean 기반의 각 Trigger 작업을 시작한다.

참고자료

2.9 - Logging

전자정부 표준프레임워크 3.0부터 SLF4J를 도입하여 다양한 로깅 프레임워크와 연계하며, Log4j 2를 기본 로깅 구현체로 사용한다. 로깅 서비스는 시스템 상태를 기록하고 관리할 수 있지만, 성능 오버헤드를 줄이기 위한 메커니즘이 필요하다. System.out.println() 대신 SLF4J와 Log4j 2를 사용한 로깅이 권장된다.

Logging 서비스

개요

전자정부 표준프레임워크 3.0부터는 다양한 Logging Framework와 연계할 수 있도록 SLF4J를 도입하였고,
Logging 구현체는 Log4j 2를 이용하여 Logging을 수행한다.

Logging 서비스는 시스템의 개발이나 운용시 발생할 수 있는 사항에 대해서,
시스템의 외부 저장소에 기록하여 시스템의 상황을 쉽게 파악할 수 있도록 도와준다.
뿐만 아니라 테스팅 코드와 운영 코드를 동일하게 가져가면서 로깅을 선언적으로 관리할 수 있다.
과도한 Logging은 운영시 성능 오버헤드를 발생시킬 수 있으므로, 최소화할 수 있는 메커니즘이 필요하다.

많은 개발자가 Log을 출력하기 위해 일반적으로 사용하는 방식은 System.out.println()이다.
하지만 이 방식은 간편한 반면, 다음과 같은 이유로 권장하지 않는다.

  • 콘솔 로그를 출력 파일로 리다이렉트 할 지라도, 어플리케이션 서버가 재시작할 때 파일이 overwrite될 수도 있음
  • 개발/테스팅 시점에만 System.out.println()을 사용하고 운영으로 이관하기 전에 삭제하는 것은 좋은 방법이 아님
  • System.out.println() 호출은 디스크 I/O동안 동기화(synchronized)처리가 되므로 시스템의 throughput을 떨어뜨림
  • 기본적으로 stack trace 결과는 콘솔에 남지만, 시스템 운영 중 콘솔을 통해 Exception을 추적하는 것은 바람직하지 못함
  • 운영시의 코드가 테스트시의 코드와 다르게 동작할 수 있음

본 페이지에서는 SLF4j와 Log4j 2에 대한 기본 사용법과 Migration에 대해 설명한다.

설명

2.10 - SLF4J

SLF4J는 다양한 로깅 프레임워크와 연계할 수 있는 추상화 계층을 제공하며, Log4j 2 또는 Logback과 함께 사용 가능하다. 기존의 Log4j 1.x나 Commons Logging을 SLF4J로 마이그레이션하려면, Bridge jar를 추가하고 logback.xml로 설정 파일을 변경해야 한다.

SLF4J

Getting Started

SLF4J(Simple Logging Facade For Java)는 특정 Logging 서비스 구현체에 종속되지 않도록 추상화 계층을 제공하며,
Jakarta Commons Logging(JCL), Log4j, Logback 등과 함께 사용할 수 있다.
다음은 SLF4J 샘플 예제이다.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Slf4JLoggerTest {

   // SLF4J를 이용한 Logger 오브젝트 생성
   private static final Logger LOGGER = LoggerFactory.getLogger(Slf4JLoggerTest.class);

   // Parameterized logging - String 타입
   String message = "Hello, eGovFrame 3.0";
   String message2 = "Welcome to eGovFrame 3.0";

   LOGGER.debug("SLF4J Logger - {}", message); // 출력결과 - SLF4J Logger - Hello, eGovFrame 3.0
   LOGGER.debug("SLF4J Logger - {} and {}", message, message2); // 출력결과 - SLF4J Logger - Hello, eGovFrame 3.0 and Welcome to eGovFrame 3.0

   // Parameterized logging - Object 타입
   Object[] args = new Object[3];
   args[0] = "1";
   args[1] = Integer.valueOf("2");
   args[2] = new Date().toString();

   LOGGER.debug("SLF4J Logger - {}, {}, {}", args); // 출력결과 - SLF4J Logger - 1, 2, Fri Mar 23 11:08:28 KST 2014
}

1. SLF4J 기본 설정

  1. SLF4J API를 사용하기 위해 slf4j-api.jar를 추가한다.
<!-- SLF4J -->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-api</artifactId>
  <version>x.x.x</version>
</dependency>
  1. Logging 충돌 방지를 위해 Spring의 Default Logging Framework인 commons-logging.jar를 제거하되,
    기존 Commons Logging API가 적절하게 변환되어 처리될 수 있도록 SLF4j JCL Bridge인 jcl-over-slf4j.jar를 추가한다.
<!-- Exclude Commons Logging in favor of SLF4J -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>${spring.maven.artifact.version}</version>
  <exclusions>
    <exclusion>
      <groupId>commons-logging</groupId>
      <artifactId>commons-logging</artifactId>
    </exclusion>
  </exclusions>
</dependency>

<!-- SLF4J JCL Bridge -->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>jcl-over-slf4j</artifactId>
  <version>x.x.x</version>
</dependency>

2. Logging 구현체 설정

  1. SLF4J가 컴파일 시에 Logging 구현체를 사용할 수 있도록 구현체별 SLF4J Binding jar와 Implementation jar를 추가한다.
Logging 구현체SLF4J Binding jar
Log4j 2log4j-slf4j-impl.jar
Log4j 1.2slf4j-log4j12.jar
JDK 1.x Loggingslf4j-jdk14.jar
NOPslf4j-nop.jar
JCLslf4j-jcl.jar
Logbacklogback-classic.jar, logback-core.jar
  • Log4j 1.2 구현체 사용 시
<!--  SLF4J Log4j1.2 Binding -->
<dependency> 
  <groupId>org.slf4j</groupId>
  <artifactId>slf4j-log4j12</artifactId>
  <version>x.x.x</version>
</dependency>

<!-- Log4j 1.2 -->
<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2</version>
</dependency>
  • Log4j 2 구현체 사용 시
<!-- Log4j2 SLF4J Binding -->
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-slf4j-impl</artifactId>
  <version>x.x.x</version>
</dependency>

<!-- Log4j 2 -->
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-api</artifactId>
  <version>x.x.x</version>
</dependency>

3. SLF4J Logger 객체 생성과 메서드 사용

  1. Logger 객체 생성
  private static final Logger LOGGER = LoggerFactory.getLogger(Slf4JLoggerTest.class);
  1. 로깅 메서드 호출
  // {}-placeholder를 이용한 Parameterized Logging
  String message = "Hello, eGovFrame 3.0";

  LOGGER.debug("SLF4J Logger - {}", message);

Migration to SLF4J from Legacy APIs

기존 Legacy API을 유지한 채 SLF4J를 함께 사용하려면, SLF4J와 레거시 API를 연결할 수 있는 Bridge jar가 필요하다.
아래에서는 Log4j 1.x와 JCL 레거시를 기준으로 설명한다.

1. Logging 구현체 jar를 SLF4J Bridge jar로 대체

이는 각 구현체의 Logging 제어권을 SLF4J로 넘긴다는 것을 의미하며, 레거시 API를 유지하기 위해서 필요한 작업이다.

  1. Log4j 1.x 유지 시, log4j.jar를 log4j-over-slf4j.jar로 대체
<!-- Log4j 1.x -->
<!-- 
<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>x.x.x</version>
</dependency>
-->

<!-- SLF4J Log4j 1.x Bridge -->  
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>log4j-over-slf4j</artifactId>
  <version>x.x.x</version>
</dependency>

(주의) log4j-over-slf4j.jar는 slf4j-log4j12.jar(SLF4j Binding)과 동시에 사용할 수 없다.

  1. JCL(Jakarta Commons Logging) 유지 시, commons-logging.jar를 jcl-over-slf4j.jar로 대체
<!-- Commons Logging -->
<!--
<dependency>
  <groupId>commons-logging</groupId>
  <artifactId>commons-logging</artifactId>
  <version>1.1.1</version>
</dependency>
-->

<!-- SLF4j JCL Bridge -->
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>jcl-over-slf4j</artifactId>
  <version>x.x.x</version>
</dependency>

2. 환경설정 파일을 logback으로 변경

log4j 환경설정 파일은 SLF4J가 인식할 수 없기 때문에, 기존 환경설정을 logback으로 변경해야한다.
log4j properties file translator 를 이용하거나 logback manual 을 참조하여 변경할 수 있다.

다음은 logback.xml 샘플이다.

<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!-- encoders are assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="debug">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

참고 자료

2.11 - Log4j2

Log4j 2는 Log4j의 차세대 버전으로서 비동기 로깅을 지원하여 애플리케이션의 성능에 미치는 영향을 최소화하고, 플러그인 아키텍처로 커스터마이징이 용이하며 다양한 출력 포맷과 로깅 레벨을 세밀하게 제어할 수 있다. XML, JSON, YAML 등 다양한 구성 파일 형식을 지원하여 설정이 유연하고, 구성 변경 시 서버를 재시작할 필요 없이 동적으로 적용할 수 있어 대규모 엔터프라이즈 애플리케이션에서도 널리 사용된다.

설명

Log4j 2 변경사항

  • Java 6 이상 필요
  • XML 환경설정 단순화 (Log4j 1.x 와 호환되지 않음)
  • Property 파일을 통한 환경설정을 지원하지 않음
  • JSON을 통한 환경설정 지원
  • 파라미터 방식으로 Log Message 출력
  • 환경설정 변경시 서버 재기동 없이 자동 재호출
  • 필터링 기능 향상
  • NoSQLAppender 등의 다양한 Appender 제공

Log4j 2 추가기능

1. Substituting Parameters

로그 메시지를 구성하는 방법으로, 기존 문자열 결합 방식과 달리 {} 안에 파라미터를 대입하여 로그 메시지를 생성하는 방법이다.
아래 코드는 출력할 로그 메시지를 완성하기 전에 Log Level을 체크하고, isDebugEnabled인 경우에만 메서드를 수행한다.

logger.debug("Logging in user {} with birthday {}", user.getName(), user.getBirthdayCalendar());

2. Formatting Parameters

Substituting Parameters 로깅 방식을 사용하면 코드 내에서 직접 formatting이 가능하다.
이 기능을 사용하려면 getFormatterLogger() 메서드를 통해 Logger 오브젝트를 생성해야 한다.
포맷 변환 문자와 형식은 Java.util.Formatter 클래스를 참조한다.

public static Logger logger = LogManager.getFormatterLogger("egovframework");

logger.debug("Logging in user %1$s with birthday %2$tm %2$te,%2$tY", user.getName(), user.getBirthdayCalendar());
logger.debug("Integer.MAX_VALUE = %,d", Integer.MAX_VALUE);
logger.debug("Long.MAX_VALUE = %,d", Long.MAX_VALUE);

// 출력결과
// User John Smith with birthday 05 23, 1995
// Integer.MAX_VALUE = 2,147,483,647
// Long.MAX_VALUE = 9,223,372,036,854,775,807

3. Flow Tracing

Log4j 2는 trace(), debug(), info() 등과 같은 로깅 메서드 뿐 아니라, 어플리케이션 실행 순서를 좀 더 쉽게 파악할 수 있도록 하는 추가적인 메서드를 제공한다.

메서드기능위치사용
entry()로그의 시작을 표시, 전달된 메서드 파라미터 출력로깅 메서드 시작부분logger.entry() or logger.entry(Object… params)
exit()로그의 끝을 표시, 리턴값 출력return문 or 로깅 메서드 끝부분logger.exit() or logger.exit(Object… result)
throwing()예외나 에러가 발생했을 때, 해당 예외/에러정보를 출력예외발생 시throw logger.throwing(new MyException);
catching()예외을 catch했을 때, 해당 예외정보를 출력catch문logger.catching(e);

이러한 메서드들이 만드는 로깅 이벤트가 기본적인 로깅 이벤트와 분리될 수 있도록 디폴트 Log Level와 Marker가 설정되어 있다.
이에 따라 entry와 exit 메서드는 TRACE 레벨에서만 출력되며 FLOW Marker를 통해 다른 로그 메세지로부터 분리(필터링)할 수 있고,
throwing과 catching 메서드는 ERROR 레벨에서만 출력되며 EXCEPTION Marker를 통해 필터링할 수 있다.

메서드Log LevelMarker
entry()TRACEENTER or FLOW
exit()TRACEEXIT or FLOW
throwing()ERRORTHROWING or EXCEPTION
catching()ERRORCATCHING or EXCEPTION
   public String saveDept(String deptNo) {

     logger.entry(deptNo); // 메서드 시작부분에 명시, 전달받은 파라미터 출력

     Dept dept = service.saveDept(deptNo);

     String nextPg = "redirect:/dept/deptList.do";

     return logger.exit(nextPg); // 메서드 종료부분에 명시, 리턴할 파라미터 출력
   }

   public static void main(String[] args) {
     saveDept("20");

     // 출력결과
     // TRACE saveDept - entry(20)
     // TRACE saveDept - exit(redirect:/dept/deptList.do)
   }

4. Markers

한꺼번에 다량의 로그가 출력되면 어느 위치에서 문제가 발생했는지 정확하게 예측할 수 없다.
또한 Log4j와 같은 Logging Framework를 사용하는 이유는 어플리케이션에서 발생하는 문제를 확인하고 디버깅 하기 위한 것이다.
이는 원하는 시점에서 로그 정보의 필터링이 가능해야함을 뜻한다.

이미 로깅 메서드와 Logger의 Log Level 설정을 통해 출력할 로그를 필터링 할 수 있지만,
Log4j 2에서는 Marker 기능을 통해 좀 더 상세한 필터링을 지원한다. 예를 들어, Flow Tracing 메서드와 기본적인 로깅 이벤트를 분리하고 싶거나, SQL문만 별도로 출력하고 싶은 경우에는 Marker 설정을 통해 기능을 구현할 수 있다.

Marker는 다음과 같은 특징을 갖는다.

  • Marker name은 유일해야 한다.
  • Marker는 final로 선언되어야 하며, 오직 하나의 Parent Marker를 갖는다.
public class MyClass {
  private static final Logger LOGGER = LogManager.getLogger(MyClass.class);

  // Marker name = "SQL"
  private static final Marker SQL_MARKER = MarkerManager.getMarker("SQL");

  // Marker name = "SQL_UPDATE", Parent marker = SQL_MARKER
  private static final Marker UPDATE_MARKER = MarkerManager.getMarker("SQL_UPDATE", SQL_MARKER);

  // Marker name = "SQL_QUERY", Parent marker = SQL_MARKER
  private static final Marker QUERY_MARKER = MarkerManager.getMarker("SQL_QUERY", SQL_MARKER);

  public String doQuery(String table) {

    LOGGER.entry(table);

    LOGGER.debug(QUERY_MARKER, "SELECT * FROM {}", table); // select.log 파일에 출력됨

    return LOGGER.exit();
}

public String doUpdate(String table, Map<String, String> params) {

    LOGGER.entry(table);

    LOGGER.debug(UPDATE_MARKER, "UPDATE {} SET {}", table, formatCols()); // update.log 파일에 출력됨

    return logger.exit();
}
...
<Appenders>
  <File name="fileQuery" fileName="./logs/file/select.log">
    <MarkerFilter marker="SQL_QUERY" onMatch="ACCEPT" onMismatch="DENY" />
    <PatternLayout pattern="%level %m%n" />
  </File>
  <File name="fileUpdate" fileName="./logs/file/update.log">
    <MarkerFilter marker="SQL_UPDATE" onMatch="ACCEPT" onMismatch="DENY" />
    <PatternLayout pattern="%level %m%n" />
  </File>
  ...
</Appenders>
...

참고자료

Flow Tracing
Markers

Migration to Log4j 2 from Log4j 1.x

1. Log4j 2 jar 추가 (log4j-api.jar, log4j-core.jar) + Log4j 1.x jar 제거

<!-- Log4j 1.2 -->
<!--
<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2</version>
</dependency>
-->

<!-- Log4j 2 -->
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-api</artifactId>
  <version>x.x.x</version>
</dependency>
<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-core</artifactId>
  <version>x.x.x</version>
</dependency>

2. Log4j 1.x -> Log4j 2 변환 Bridge jar 추가 (log4j-1.2-api.jar)

기존 Log4j 1.x API가 Log4j 2 API로 변환 처리될 수 있도록 Log4j 2 Bridge를 추가한다.

<dependency>
  <groupId>org.apache.logging.log4j</groupId>
  <artifactId>log4j-1.2-api</artifactId>
  <version>x.x.x</version>
</dependency>

3. Log4j 1.x의 Logger API 변경

Log4j 2의 Logger 객체를 생성할 수 있도록, Logger 생성 메서드를 다음과 같이 변경한다.

Log4j 1.xLog4j 2
Packageorg.apache.log4jorg.apache.logging.log4j
Logger 생성org.apache.log4j.Logger logger = org.apache.log4j.Logger.getLogger();org.apache.logging.log4j.Logger logger = org.apache.logging.log4j.LogManager.getLogger();

4. Log4j 2 설정 추가 (log4j2.xml)

Log4j 2에서는 설정 태그들이 직관적이고 간단하게 변경되었다. 더 자세한 설명은 Log4j 2 상세 설정 을 참조하도록 한다.

  • Log4j 1.x
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration PUBLIC "-//APACHE//DTD LOG4J 1.2//EN" "log4j.dtd">
<log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'>
  <appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
    <layout class="org.apache.log4j.PatternLayout">
      <param name="ConversionPattern" value="%d %-5p [%t] %C{2} (%F:%L) - %m%n"/>
    </layout>
  </appender>
  <category name="org.apache.log4j.xml">
    <priority value="info" />
  </category>
  <Root>
    <priority value ="debug" />
    <appender-ref ref="STDOUT" />
  </Root>
</log4j:configuration>
  • Log4j 2
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
  <Appenders>
    <Console name="STDOUT" target="SYSTEM_OUT">
      <PatternLayout pattern="%d %-5p [%t] %C{2} (%F:%L) - %m%n"/>
    </Console>
  </Appenders>
  <Loggers>
    <Logger name="org.apache.log4j.xml" level="info"/>
    <Root level="debug">
      <AppenderRef ref="STDOUT"/>
    </Root>
  </Loggers>
</Configuration>

참고자료

Migration to Log4j 2
Log4j 2 API Documentation
Log4j 2 Implementation Documentation

2.12 - Log4j 2 환경설정 (코드 내에서 직접 설정 시)

Log4j 2 환경설정을 코드 내에서 직접 제어하여 Logger 객체를 생성하고, 기본 설정을 수정할 수 있다. 예시에서는 ConsoleAppender를 FileAppender로 변경하고 로그 레벨을 DEBUG로 설정하는 방법을 설명한다. Logger 설정 관련 메서드들을 사용하여 Appender와 Layout, Log Level을 동적으로 변경할 수 있다.

Log4j 2 환경설정 (코드 내에서 직접 설정 시)

개요

Log4j 2 환경 설정(Appender, Layout, Log Level 등)을 코드 내에서 직접 제어할 수 있다.
아래는 별도의 외부 설정파일 없이도 로깅할 수 있는 방법을 설명한다.

설명

사용 방법

별도의 Log4j 2 설정파일 없이도 코드 내에서 Logger 객체를 획득하여 로깅이 가능하다.
LogManager.getLogger() 메서드를 통해 Logger 객체를 생성하며, Log4j 2는 디폴트로 설정된 Logger 객체를 반환한다.
디폴트 Logger 객체의 기본적인 디폴트 설정은 다음과 같다.

  Log Level : ERROR
  Appender  : ConsoleAppender
  Layout    : PatternLayout
  pattern   : %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n

위 Logger 객체를 org.apache.logging.log4j.core.Logger로 캐스팅하면, Log Level, Appender, Layout 등 설정을 변경할 수 있다.
예시에서 사용된 설정 관련 메서드는 다음과 같다. 더 자세한 정보는 Log4j 2 API를 참조하도록 한다.

  LogManager.getLogger()                                                -- Logger 생성
  Logger.addAppender(), Logger.removeAppender(), Logger.getAppenders()  -- Logger에 Appender 추가  제거, 획득
  Layout.createLayout()                                                 -- Layout 생성
  Appender.createAppender(), Appender.removeAppender()                  -- Appender 생성  삭제
  Logger.setLevel(), Logger.getLevel()                                  -- Log Level 설정  획득

사용 예시

@Test
public void log4j2ConfigTest() {

	// 디폴트 Logger 생성
	Logger logger = (Logger) LogManager.getLogger();

	// 디폴트 설정 확인
	// Log Level: ERROR
	assertEquals(Level.ERROR, logger.getLevel());
	// Appender: Console
	Map<String, Appender> appenders = logger.getAppenders();
	assertEquals(1, appenders.size()); 
	assertEquals(ConsoleAppender.class, appenders.get("Console").getClass());
	// Layout: Pattern
	ConsoleAppender console = (ConsoleAppender) appenders.get("Console");
	assertEquals(PatternLayout.class, console.getLayout().getClass());
	PatternLayout pattern = (PatternLayout) console.getLayout();
	assertEquals("%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n", pattern.toString());

	// 설정 변경 전: ERROR Level 이상 로그만 출력됨
	logger.debug("변경 전: [DEBUG] Test log4j 2.");
	logger.info("변경 전: [INFO] Test log4j 2.");
	logger.warn("변경 전: [WARN] Test log4j 2.");
	logger.error("변경 전: [ERROR] Test log4j 2.");
	logger.fatal("변경 전: [FATAL] Test log4j 2.");

	/*
	출력결과
	===================
	14:17:09.930 [main] ERROR egovframework.rte.fdl.logging.LogTest - 변경 전: [ERROR] Test log4j 2.
	14:17:09.931 [main] FATAL egovframework.rte.fdl.logging.LogTest - 변경 전: [FATAL] Test log4j 2
	*/

	// 디폴트 설정 변경
	logger.removeAppender(console);
	// Appender: File
	PatternLayout layout = PatternLayout.createLayout("%m%n", null, null, null, null, null);
	FileAppender file = FileAppender.createAppender("./logs/file/promatic.log", "false", "false", "file", "false", "true", "false", null, layout, null, "false", null, null); 
	logger.addAppender(file);

	// Log Level: DEBUG
	logger.setLevel(Level.DEBUG);

	// 설정 변경 후: DEBUG Level 이상 로그가 file(promatic.log)에 출력됨
	logger.debug("변경 후: [DEBUG] Test log4j 2.");
	logger.info("변경 후: [INFO] Test log4j 2.");
	logger.warn("변경 후: [WARN] Test log4j 2.");
	logger.error("변경 후: [ERROR] Test log4j 2.");
	logger.fatal("변경 후: [FATAL] Test log4j 2.");

	/* 
	출력결과
	===================
	변경 후: [DEBUG] Test log4j 2.
	변경 후: [INFO] Test log4j 2.
	변경 후: [WARN] Test log4j 2.
	변경 후: [ERROR] Test log4j 2.
	변경 후: [FATAL] Test log4j 2. 
	*/

}

참고자료

Apache Log4j Core 2.0-rc1 API

2.13 - Log4j 2 환경설정 (설정 파일 사용 시)

Log4j 2는 XML 형식의 설정 파일을 사용하여 Logger와 Appender, Layout 등을 정의할 수 있으며, 이를 통해 로그의 출력 위치와 형식을 설정할 수 있다. Logger는 로그 레벨과 Appender를 통해 로그의 출력 위치와 레벨을 제어하며, RollingFileAppender와 같은 다양한 Appender를 지원한다. Layout을 통해 로그 출력 형식을 지정하며, 특히 PatternLayout을 사용해 로그 메시지의 포맷을 세밀하게 설정할 수 있다.

Log4j 2 환경설정 (설정 파일 사용 시)

개요

Log4j 2는 기존 Properties 파일 형식의 환경 설정을 지원하지 않으며,
XML (log4j2.xml) 혹은 JSON (log4j2.json or log4j2.jsn) 파일 형식의 환경 설정만 가능하다.

아래는 XML 파일을 이용한 환경 설정에 대해서만 다루며, JOSN 방식은 Log4j 2 매뉴얼을 참고하도록 한다.

설명

Log4j 2 XML Configuration

XML 파일 위치

XML 파일(log4j2.xml)을 작성하고, WEB-INF/classes 하위에 포함될 수 있도록 위치시킨다.
Log4j 2가 초기화될 때 자동으로 위 설정 파일을 읽어들인다.

XML 파일 정의

Log4j 2에서는 XML 파일의 최상위 요소가 <Configuration> 으로 변경되었다.
<Configuration> 요소 아래에 Logger, Appender, Layout 설정 등과 관련한 하위 요소를 정의한다.

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>

 <!-- Appender, Layout 설정 -->
 <Appenders>
  <Console name="console" target="SYSTEM_OUT">
   <PatternLayout/>
  </Console>
  <File name="file" fileName="./logs/file/sample.log" append="false">
   <PatternLayout pattern="%d %5p [%c] %m%n"/>
  </File>
 </Appenders>

 <!-- Logger 설정 -->
 <Loggers>
  <Logger name="egovLogger" level="DEBUG" additivity="false">
   <AppenderRef ref="console"/>
   <AppenderRef ref="file"/>
  </Logger>
  <Rootlevel="ERROR">
   <AppenderRef ref="console"/>
  </Root>
 </Loggers>

</Configuration>

참고자료

Log4j 2 Configuration
XML Syntax
JSON Syntax

Logger 설정

Logger는 로깅 작업을 수행하는 Log4j 주체로, Logger 설정을 제외한 모든 로깅 기능이 이 Logger를 통해 처리된다.
사용자는 어플리케이션 내에서 사용할 Logger를 정의해야 하며, Log Level과 Appender 설정에 따라 출력 대상과 위치가 결정된다.

Logger 선언과 정의

Root Logger를 포함한 모든 Logger는 상위 요소인 <Loggers> 아래에 선언한다.
Root Logger는 <Root> 요소로, 일반 Logger는 <Logger> 요소로 정의한다.
Logger는 하나 이상 정의할 수 있으며, Root 요소를 반드시 정의해야 한다.

 <Loggers>
  <Logger>...</Logger>
  <Root>...</Root>
 </Loggers>
 <Loggers>
  <!-- attribute: name(Logger명), level(Log Level), additivity(중복로깅여부, true or false) -->
  <!-- element: AppenderRef(Appender명) -->
  <Logger name="X.Y" level="INFO" additivity="false">
   <AppenderRef ref="console"/>  
  </Logger>
  <Logger name="X" level="DEBUG" additivity="false">
   <AppenderRef ref="console"/>  
  </Logger>
  <Rootlevel="ERROR">
   <AppenderRef ref="console"/>
  </Root>
 </Loggers>

위에서 AppenderRef 요소에 지정한 “console” Appender가 없는 경우, 정상적인 로깅이 수행될 수 없다.

Logger 호출

Logger는 코드 내에서 다음과 같은 방법으로 호출할 수 있다.

  package egovframe.sample;

  import org.apache.logging.log4j.LogManager;
  import org.apache.logging.log4j.Logger;

  public class LoggerTest {

   // (1) Logger Name이 "egovframe.sample.LoggerTest"인 Logger 설정을 따르는 Logger 객체 생성
   Logger logger1 = LogManager.getLogger();     
   // (2) 위와 동일             
   Logger logger2 = LogManager.getLogger(LoggerTest.class);  
   // (3) Logger Name이 "X"인 Logger설정을 따르는 Logger 객체 생성
   Logger logger3 = LogManager.getLogger("X");   

  }

(1), (2)과 같이 Logger Name에 해당하는 Logger가 설정 파일에 없는 경우, 다음 Logger Hierarchy 규칙에 따라 결정된다.
결과적으로 (1), (2)에서 생성된 Logger 객체는 Root Logger 설정을 따른다.

Logger Hierarchy

사용자가 호출한 Logger 객체가 어떤 설정을 따르는지 이해하기 위해서는 Logger Hierarchy를 알고 있어야 한다.
내부적으로 설정 파일에 정의된 각 Logger 설정에 따라 LoggerConfig 오브젝트가 생성되며,
Logger Name에 따라 오브젝트 간 부모-자식 관계가 성립한다. 즉 부모 Logger의 설정을 자식 Logger가 상속받는다.
예를 들어 “X.Y” Logger의 부모는 “X"이고, “X” Logger의 부모는 Root Logger(최상위)이다.

다음은 Hierarchy 규칙과 예시이다.

  1. 호출한 Logger Name과 동일한 Logger가 있는 경우, 해당 Logger 설정을 따른다.
  2. 동일한 Logger는 없지만, Parent Logger가 존재하는 경우, Parent Logger 설정을 따른다.
  3. Parent Logger도 존재하지 않는 경우, Root Logger 설정을 따른다.
Logger NameAssigned LoggerConfigLevelJava CodeDescription
rootrootERRORLogManager.getLogger(“root”);설정 파일의 Root 설정을 따름
XXDEBUGLogManager.getLogger(“X”);설정 파일의 X Logger 설정을 따름
X.YX.YINFOLogManager.getLogger(“X.Y”);설정 파일의 X.Y Logger 설정을 따름
X.Y.ZX.YINFOLogManager.getLogger(“X.Y.Z”);X.Y.Z Logger 설정이 없으므로, 부모인 X.Y 설정을 따름
X.YZXDEBUGLogManager.getLogger(“X.YZ”);X.YZ Logger 설정이 없으므로, 부모인 X 설정을 따름
YrootERRORLogManager.getLogger(“Y”);Y Logger 설정이 없으므로, 부모인 Root 설정을 따름

Log Level

Log4j 2는 FATAL, ERROR, WARN, INFO, DEBUG, TRACE의 Log Level을 제공한다.
각각 trace(), debug(), info(), warn(), error(), fatal()라는 로깅 메서드를 이용해 로그를 출력할 수 있다.
로그 레벨은 다음과 같다. (FATAL > ERROR > WARN > INFO > DEBUG > TRACE)

로그 레벨설명
FATAL아주 심각한 에러가 발생한 상태를 나타냄. 시스템적으로 심각한 문제가 발생해서 어플리케이션 작동이 불가능할 경우가 해당하는데, 일반적으로는 어플리케이션에서는 사용할 일이 없음.
ERROR요청을 처리하는중 문제가 발생한 상태를 나타냄.
WARN처리 가능한 문제이지만, 향후 시스템 에러의 원인이 될 수 있는 경고성 메시지를 나타냄.
INFO로그인, 상태변경과 같은 정보성 메시지를 나타냄.
DEBUG개발 시 디버그 용도로 사용한 메시지를 나타냄.
TRACE디버그 레벨이 너무 광범위한 것을 해결하기 위해서 좀더 상세한 상태를 나타냄.

어플리케이션 수행 중 Log Level을 변경할 수도 있다. 이 때 Logger Configuration을 변경하는 것이므로, Logger 설정 정보를 참조하는 메서드를 호출할 수 있도록 org.apache.logging.log4j.Logger를 org.apache.logging.log4j.core.Logger로 캐스팅해야 한다.

Log Level 변경하려면 변경할 Level값을 파라미터로 setLevel() 메서드를 호출한다.
setLevel() 호출 이후부터 Log Level이 변경되며, 지정된 로그레벨 이하의 Log Event는 무시된다.

 package egovframe.sample;

 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;

 public class LoggerTest {
  Logger logger = LogManager.getLogger(); // Root Logger 설정을 따름, Log Level: ERROR
  org.apache.logging.log4j.core.Logger targetLogger = (org.apache.logging.log4j.core.Logger) logger;

  targetLogger.debug("변경 전 - debug"); // 출력됨
  targetLogger.error("변경 전 - error"); // 출력 안됨


  targetLogger.setLevel(Level.DEBUG); // DEBUG, INFO, WARN, ERROR, FATAL 출력 가능
  targetLogger.debug("변경 후 - debug"); // 출력됨
  targetLogger.error("변경 후 - error"); // 출력됨	
}

자바에서는 C와 같이 전처리기의 기능이 없기 때문에 #ifdef DEBUG와 같은 형태와 같이 디버깅 때와 릴리즈 때의 디버깅코드를 각각 별도로 생성할 수가 없다. 따라서 Log4j의 이러한 기능은 로그관리에 있어서 상당히 편리하다.

참고자료

자세한 설정은 Log4j 2 Logger 매뉴얼을 참고하도록 한다.

Appender 설정

Appender는 로그가 출력되는 위치를 나타낸다.
XXXAppender로 끝나는 클래스들의 이름을 보면, 출력 위치를 어느 정도 짐작할 수 있다.

Log4j 2는 Console, File, RollingFile, Socket, DB 등 다양한 로그 출력 위치와 방법을 지원한다.
기존 Log4j 1.x와 크게 달라진 점은 Appender 종류를 class 속성값으로 구분한 것과 달리, Log4j 2에서는 태그로 구분한다.

Appender 선언과 정의

본 페이지에서는 자주 사용되는 Console, File, RollingFile, JDBC Appender에 대해서만 설명한다.
출력 위치에 따라 Appender 종류와 설정 태그가 달라지며, 아래 표는 각 Appender 정의 태그와 출력 위치이다.

Appenders태그명출력 위치
ConsoleAppender<Console>콘솔에 출력
FileAppender<File>파일에 출력
RollingFileAppender<RollingFile>조건에 따라 파일에 출력
JDBCAppender<JDBC>RDB Table에 출력

모든 Appender 요소는 상위 요소인 <Appenders> 아래에 선언한다.

 <Appenders>
  <Console>...</Console>
  <File>...</File>
  <RollingFile>...</RollingFile>
  <JDBC>...</JDBC>
 </Appenders>

Appender 요소는 name 속성값을 가지며, name 속성에 Appender 이름을 지정한다. name 속성값은 Logger가 로그 출력에 사용할 Appender를 참조하기 위해 사용한다. 또한 Appender 요소의 하위 요소로 로그 출력 패턴인 Layout을 정의한다.
아래는 ConsoleAppender와 PatternLayout을 사용한 샘플코드이다.

 <Appenders>
   <Console name="console" target="SYSTEM_OUT">
    <PatternLayout /> <!-- 디폴트 패턴 적용, %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n -->
  </Console>
 </Appenders>
 <Loggers>
  <Logger name="egovLogger" level="DEBUG" additivity="false">
   <AppenderRef ref="console" />
  </Logger>
  <Rootlevel="ERROR">
   <AppenderRef ref="console" />
  </Root>
 </Loggers>

Appender 종류

다음은 각 Appender 정의 시 필요한 기본 설정에 대한 설명이다.

ConsoleAppender

로그를 콘솔에 출력하기 위한 Appender

 <!-- attribute: name(Appender명), target(출력방향지정, "SYSTEM_OUT" or "SYSTEM_ERR"(default)), follow, ignoreExceptions -->
 <!-- element: Layout(출력패턴설정), Filters -->
 <Console name="console" target="SYSTEM_OUT">
  <PatternLayout pattern="%d %5p [%c] %m%n" />
 </Console>
FileAppender

로그를 파일에 출력하기 위한 Appender

 <!-- attribute: name(Appender명), fileName(target파일명), append(이어쓰기여부, true(default) or false), locking, immediateFlush, ignoreExceptions, bufferedIO -->
 <!-- element: Layout(출력패턴설정), Filters -->
 <!-- append="false"이면 매번 로깅 시 기존 로그 파일을 clear하고 새로 로깅 -->
 <File name="file" fileName="./logs/file/sample.log" append="false">
  <PatternLayout pattern="%d %5p [%c] %m%n" />
 </File>	
 <File name="mdcFile" fileName="./logs/file/mdcSample.log" append="false">
  <!-- Thread Context Map(also known as MDC) 객체의 key와 매칭되는 value를 로깅 - %X{key} -->
  <!-- ex) ThreadContext.put(“testKey”, “testValue”);인 경우, 레이아웃 패턴 %X{testKey}에 의해 “testValue” 로깅 -->
  <PatternLayout pattern="%d %5p [%c] [%X{class} %X{method} %X{testKey}] %m%n" />
 </File>
RollingFileAppender

TriggeringPolicy와 RolloverStrategy에 따라 로그를 파일에 출력하기 위한 Appender로,
FileAppender는 지정한 파일에 로그가 계속 남으므로 한 파일의 크기가 지나치게 커질 수 있고, 계획적인 로그관리가 불가능해진다.
그러나 RollingFileAppender는 파일의 크기 또는 파일 백업 인덱스 등의 지정을 통해서 특정 크기 이상으로 파일 크기가 커지게 되면,
기존파일(target)을 백업파일(history)로 바꾸고, 다시 처음부터 로깅을 시작한다.

 <!-- attribute: name(Appender명), fileName(target파일명), filePattern(history파일명), append, immediateFlush, ignoreExceptions, bufferedIO -->
 <!-- element: Layout(출력패턴설정), Filters, Policy(file rolling 조건 설정), Strategy(file name과 location 관련 설정) -->
 <RollingFile name="rollingFile" fileName="./logs/rolling/rollingSample.log" filePattern="./logs/rolling/rollingSample.%i.log">
  <PatternLayout pattern="%d %5p [%c] %m%n" />
  <Policies>
   <!-- size 단위: Byte(default), KB, MB, or GB -->
   <SizeBasedTriggeringPolicy size="1000" />
  </Policies>
  <!-- 기존 maxIndex 속성이 Strategy 엘리먼트로 변경됨 -->
  <!-- index는 min(default 1)부터 max(default 7)까지 증가, 아래에는 max="3"으로 settting -->
  <!-- fileIndex="min"이므로 target file의 size가 1000 byte를 넘어가면, fileIndex가 1(min)인 history file에 백업 (fixed window strategy) -->
  <!-- 그 다음 1000 byte를 넘어가면, rollingSample.1.log을 rollingSample.2.log 파일에 복사하고, target 파일을 rollingSample.1.log에복사한 후 target 파일에 새로 로깅 -->
  <DefaultRolloverStrategy max="3" fileIndex="min" />
 </RollingFile>
  • DailyRollingFileAppender
    기존 DailyRollingFileAppender가 삭제되고, RollingFileAppender에서 <TimeBasedTriggeringPolicy> 엘리먼트로 설정 가능하도록 변경되었다. 설정한 날짜 또는 조건에 맞춰 로깅을 수행하며, interval 속성을 이용해 rolling 간격을 지정할 수 있다.
 <RollingFile name="rollingFile" fileName="./logs/rolling/dailyRollingSample.log" filePattern="./logs/daily/dailyRollingSample.log.%d{yyyy-MM-dd-HH-mm-ss}">
  <PatternLayout pattern="%d %5p [%c] %m%n" />
  <Policies>
   <!-- interval(default 1)이므로 1초 간격으로 rolling 수행 --> 
   <TimeBasedTriggeringPolicy />
  </Policies>
 </RollingFile>
JDBCAppender

로그를 RDB에 출력하기 위한 Appender로,
Connection 객체를 제공하기 위한 JNDI DataSource 혹은 Connection Factory Method를 함께 정의해야한다.

 <!-- attribute: name(Appender명), tableName(RDB Table명), columnConfigs, filter, bufferSize, ignoreExceptions, connectionSource -->
 <!-- element: DataSource(jndi datasource 정보), ConnectionFactory(Connection Factory 정보), Column(Table Column명) -->
 <!-- 테이블명이 db_log인 테이블에 로깅됨 -->
 <JDBC name="db" tableName="db_log">
  <!-- DB Connection을 제공해줄 클래스와 메서드명 정의 -->
  <!-- JDBCAppender가 EgovConnectionFactory.getDatabaseConnection() 메서드를 호출 -->
  <ConnectionFactory class="egovframework.rte.fdl.logging.db.EgovConnectionFactory" method="getDatabaseConnection" />
   <!-- log event가 insert될 컬럼 설정, insert될 값은 PatternLayout의 pattern 사용 -->
   <Column name="eventDate" isEventTimestamp="true" />
   <Column name="level" pattern="%p" />
   <Column name="logger" pattern="%c" />
   <Column name="message" pattern="%m" />
   <Column name="exception" pattern="%ex{full}" />
 </JDBC>

표준프레임워크에서는 Connection Factory 역할을 하는 EgovConnectionFactory를 제공하고 있으며, 어플리케이션에서 설정한 dataSource 빈을 주입받아 싱글톤을 생성한다. 이를 위해 다음과 같이 빈정의를 추가해야한다.

 <bean id="egovConnectionFactory" class="egovframework.rte.fdl.logging.db.EgovConnectionFactory">
  <property name="dataSource" ref="dataSource" />
 </bean>

WAS에서 제공하는 DataSource를 사용하려면, 위 <ConnectionFactory> 부분을 <DataSource jndiName=”…” />으로 변경한다.

참고자료

각 Appender 요소에서 정의할 수 있는 하위 요소와 속성이 다르므로, 자세한 설정은 각 매뉴얼을 참조하도록 한다.

Log4j 2 Appneders
ConsoleAppender
FileAppender
RollingFileAppender
JDBCAppender

Layout 설정

Layout은 발생한 로그 이벤트의 포맷을 지정하고, 원하는 형식으로 로그를 출력할 수 있다.
Appenders 설정과 마찬가지로 Log4j 2에서는 Layout을 class 속성이 아닌 태그로 구분한다.

출력 형식에 따라 Layout의 종류가 달라지며, 아래와 같은 Layouts을 제공한다.

내용
HTMLLayoutPatternLayoutRFC5424LayoutSerializedLayoutSyslogLayoutXMLLayout

본페이지는 위의 Layouts 중 일반적으로 디버깅에 가장 적합한 PatternLayout만 설명한다.

PatternLayout 선언과 정의

PatternLayout은 Appender 요소의 하위 요소로 정의한다.

 <Console>
  <!-- 디폴트 패턴 적용, "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" -->
  <PatternLayout/>
 </Console>

<PatternLayout/>을 선언하면 디폴트 pattern이 적용되며, pattern 속성을 이용하여 일자, 시간, 클래스, 로거명, 메시지 등 여러 정보를 선택하여 다양한 조합의 로그 메시지를 출력할 수 있다.

PatternLayout의 pattern

%로 시작하고 %뒤에는 format modifiers와 conversion character로 정의한다.
예) %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n

패턴설명
c, logger로깅 이벤트를 발생시키기 위해 선택한 로거의 이름을 출력
C, class로깅 이벤트가 발생한 클래스의 풀네임명을 출력
M, method로깅 이벤트가 발생한 메서드명을 출력
F, file로깅 이벤트가 발생한 클래스의 파일명을 출력
l, location로깅 이벤트가 발생한 클래스의 풀네임명.메서드명(파일명:라인번호)를 출력
d, date로깅 이벤트의 일자와 시간을 출력,\\SimpleDateFormat클래스에 정의된 패턴으로 출력 포맷 지정가능
L, line로깅 이벤트가 발생한 라인 번호를 출력
m, msg, message로그문에서 전달된 메시지를 출력
n줄바꿈
p, level로깅 이벤트의 레벨을 출력
r, relative로그 처리시간 (milliseconds)
t, thread로깅 이벤트가 발생한 스레드명을 출력
%%%를 출력하기 위해 사용하는 패턴

참고자료

Log4j 2 Layouts
PatternLayout

참고자료

Apache Logging Services Project

2.14 - Id Generation 서비스

ID Generation 서비스는 UUID, Sequence, Table 기반의 고유 ID 생성을 지원하며, UUID는 MAC/IP 주소 또는 랜덤 방식으로, Sequence는 DB 시퀀스를 사용해 ID를 생성한다. Table ID Generation은 별도의 테이블을 사용해 ID를 관리하며, Strategy 설정을 통해 ID 생성 규칙을 지정할 수 있다. 다양한 설정을 통해 ID를 쉽게 생성하고 관리할 수 있도록 지원한다.

Id Generation 서비스

개요

시스템을 개발할 때 필요한 유일한 ID를 생성하기 위해 사용하도록 서비스한다.

주요 개념

Universally Unique Identifier(UUID)

UUID는 OSF(Open Software Foundation)에 의해 제정된 고유식별자(Identifier)에 대한 표준이다. UUID는 16-byte (128-bit)의 숫자로 구성된다.

UUID를 표현하는 방식에 대한 특별한 규정은 없으나, 일반적으로 아래와 같이 16진법으로 8-4-4-4-12 형식으로 표현한다.

550e8400-e29b-41d4-a716-446655440000

UUID 표준은 아래 문서에 기술되어 있다.

UUID는 다음 5개의 Version이 존재한다.

  • Version 1 (MAC Address)
    • UUID를 생성시키는 컴퓨터의 MAC 어드레스와 시간 정보를 이용하여 UUID를 생성한다.
    • 컴퓨터의 MAC 어드레스를 이용하므로 어떤 컴퓨터에서 생성했는지 정보가 남기 때문에 보안에 문제가 있다.
  • Version 2 (DCE Security)
    • POSIX UID를 이용하여 UUID를 생성한다.
  • Version 3 (MD5 Hash)
    • URL로부터 MD5를 이용하여 UUID를 생성한다.
  • Version 4 (Random)
    • Random Number를 이용하여 UUID를 생성한다.
  • Version 5 (SHA-1 Hash)
    • SHA-1 Hashing을 이용하여 UUID를 생성한다.

설명

ID 생성 방식으로는 UUID를 생성하는 UUID Generation Service와 sequence를 활용하는 Sequence Id Generation Service, 그리고 키제공을 위한 테이블을 지정하여 생성하는 Table Id Generation Service 3가지가 있다.

UUID Generation Service

새로운 ID를 생성하기 위해 UUID 생성 알고리즘을 이용하여 16 바이트 길이의 ID를 생성한다.

String 타입의 ID 생성과 BigDecimal 타입의 ID 생성 두가지 유형 ID 생성을 지원한다.

지원하는 방법은 설정에 따라서 Mac Address Base Service, IP Address Base Service, No Address Base Service 세가지 유형이 있다.

Mac Address Base Service

MAC Address를 기반으로 유일한 Id를 생성하는 UUIdGenerationService

Configuration
<bean name="UUIdGenerationService" class="org.egovframe.rte.fdl.idgnr.impl.EgovUUIdGnrServiceImpl">
    <property name="address">
        <value>00:00:F0:79:19:5B</value>
    </property>
</bean>
Sample Source
@Resource(name="UUIdGenerationService")
private EgovIdGnrService uUidGenerationService;
 
@Test
public void testUUIdGeneration() throws Exception {
   assertNotNull(uUidGenerationService.getNextStringId());
   assertNotNull(uUidGenerationService.getNextBigDecimalId());
}

IP Address Base Service

IP Address를 기반으로 유일한 Id를 생성하는 UUIdGenerationService

Configuration
<bean name="UUIdGenerationServiceWithIP" class="org.egovframe.rte.fdl.idgnr.impl.EgovUUIdGnrServiceImpl">
    <property name="address">
        <value>100.128.120.107</value>
    </property>
</bean>
Sample Source
@Resource(name="UUIdGenerationServiceWithIP")
private EgovIdGnrService uUIdGenerationServiceWithIP; 
 
@Test
public void testUUIdGenerationIP() throws Exception {
   assertNotNull(uUIdGenerationServiceWithIP.getNextStringId());
   assertNotNull(uUIdGenerationServiceWithIP.getNextBigDecimalId());
}

No Address Base Service

IP Address 설정없이 Math.random()을 이용하여 주소정보를 생성하고 유일한 Id를 생성하는 UUIdGenerationService

Configuration
<bean name="UUIdGenerationServiceWithoutAddress" class="org.egovframe.rte.fdl.idgnr.impl.EgovUUIdGnrServiceImpl">
</bean>
Sample Source
@Resource(name="UUIdGenerationServiceWithoutAddress")
private EgovIdGnrService uUIdGenerationServiceWithoutAddress; 
 
@Test
public void testUUIdGenerationNoAddress() throws Exception {
   assertNotNull(uUIdGenerationServiceWithoutAddress.getNextStringId());
   assertNotNull(uUIdGenerationServiceWithoutAddress.getNextBigDecimalId());
}

Sequence Id Generation Service

새로운 ID를 생성하기 위해 Database의 SEQUENCE를 사용하는 서비스이다. 서비스를 이용하는 시스템에서 Query를 지정하여 아이디를 생성할 수 있도록 하고 Basic Type ServiceBigDecimal Type Service 두가지를 지원한다.

Basic Type Service

기본타입 ID를 제공하는 서비스로 int, short, byte, long 유형의 ID를 제공한다.

DB Schema
CREATE SEQUENCE idstest MINVALUE 0;
Configuration
<bean name="primaryTypeSequenceIds" class="org.egovframe.rte.fdl.idgnr.impl.EgovSequenceIdGnrServiceImpl" destroy-method="destroy">
  <property name="dataSource" ref="dataSource"/>
  <property name="query" value="SELECT idstest.NEXTVAL FROM DUAL"/>
</bean>
Sample Source
@Resource(name="primaryTypeSequenceIds")
private EgovIdGnrService primaryTypeSequenceIds;
 
@Test
public void testPrimaryTypeIdGeneration() throws Exception {
   //int
   assertNotNull(primaryTypeSequenceIds.getNextIntegerId());
   //short
   assertNotNull(primaryTypeSequenceIds.getNextShortId());
   //byte
   assertNotNull(primaryTypeSequenceIds.getNextByteId());
   //long
   assertNotNull(primaryTypeSequenceIds.getNextLongId());
}

BigDecimal Type Service

BigDecimal ID를 제공하는 서비스로 기본타입 ID 제공 서비스 설정에 추가적으로 useBigDecimalstrue로 설정하여 BigDecimal 사용하도록 한다.

DB Schema
CREATE SEQUENCE idstest MINVALUE 0;
Configuration
<bean name="bigDecimalTypeSequenceIds" class="org.egovframe.rte.fdl.idgnr.impl.EgovSequenceIdGnrServiceImpl" destroy-method="destroy">
  <property name="dataSource" ref="dataSource"/>
  <property name="query" value="SELECT idstest.NEXTVAL FROM DUAL"/>
  <property name="useBigDecimals" value="true"/>
</bean>
Sample Source
@Resource(name="bigDecimalTypeSequenceIds")
private EgovIdGnrService bigDecimalTypeSequenceIds;
 
@Test
public void testBigDecimalTypeIdGeneration() throws Exception {
   //BigDecimal
   assertNotNull(bigDecimalTypeSequenceIds.getNextBigDecimalId());
}

Database 별 설정

Oracle
  • DB Schema
CREATE SEQUENCE <sequence name> [START WITH <start value>] [INCREMENT BY <increment value>] [MINVALUE <min value>] [MAXVALUE <max value>]
  • Query
SELECT <sequence name>.NEXTVAL FROM DUAL
HSQL
  • DB Schema
CREATE SEQUENCE <sequence name> [AS {INTEGER | BIGINT}] [START WITH <start value>] [INCREMENT BY <increment value>]
  • Query
SELECT NEXT VALUE FOR <sequence name> FROM DUAL
-- HSQL DB는 DUAL 테이블을 제공하지 않기 때문에 하나의 row를 가진 DUAL 테이블을 수동으로 생성해야 한다.
IBM DB2
  • DB Schema
CREATE SEQUENCE <sequence name> [START WITH <start value>] [INCREMENT BY <increment value>] [MINVALUE <min value>] [MAXVALUE <max value>]
  • Query
SELECT NEXT VALUE FOR <sequence name> FROM SYSIBM.SYSDUMMY1
Sybase
  • Sybase는 Sequence를 지원하지 않는다.
MS SQL Server
  • MS SQL Server는 Sequence를 지원하지 않는다.
MySQL
  • MySQL은 Sequence를 지원하지 않는다.

Table Id Generation Service

새로운 아이디를 얻기 위해 별도의 테이블을 생성, 키값과 키값에 해당하는 아이디값을 입력하여 관리를 제공하는 서비스이다.

table_name(CHAR 또는 VARCHAR타입), next_id(integer 또는 DECIMAL type)와 같이 두 칼럼을 필요로 한다.

별도의 테이블에 설정된 정보만을 사용하여 제공하는 Basic Service, prefix와 채울 문자열을 지정하여 String ID를 생성할 수 있는 Strategy Base Service를 제공한다.

Basic Service

테이블에 지정된 정보에 의해서 아이디를 생성하는 서비스로 사용하고자 하는 시스템에서 테이블을 생성해서 사용할 수 있다.

DB Schema
CREATE TABLE ids (
  table_name varchar(16) NOT NULL, 
  next_id DECIMAL(30) NOT NULL,
  PRIMARY KEY (table_name)
);
INSERT INTO ids VALUES('id','0');
-- ID Generation 서비스를 쓰고자 하는 시스템에서 미리 생성해야 할 DB Schema 정보이다.
Configuration
<bean name="basicService" class="org.egovframe.rte.fdl.idgnr.impl.EgovTableIdGnrServiceImpl" destroy-method="destroy">
  <property name="dataSource" ref="dataSource"/>
  <property name="blockSize" value="10"/>
  <property name="table" value="ids"/>
  <property name="tableName" value="id"/>
</bean>
  • blockSize
    • Id Generation 내부적으로 사용하는 정보로 ID 요청시마다 DB접속을 하지 않기 위한 정보
      • 지정한 횟수 마다 DB 접속 처리
  • table
    • 생성하는 테이블 정보로 사용처에서 테이블명 변경 가능
  • tableName
    • 사용하고자 하는 아이디 개별 인식을 위한 키 값
      • 테이블 별로 아이디가 필요하기에 tableName이라고 지정함
Sample Source
@Resource(name="basicService")
private EgovIdGnrService basicService;
 
@Test
public void testBasicService() throws Exception {
   //int
   assertNotNull(basicService.getNextIntegerId());
   //short
   assertNotNull(basicService.getNextShortId());
   //byte
   assertNotNull(basicService.getNextByteId());
   //long
   assertNotNull(basicService.getNextLongId());
   //BigDecimal
   assertNotNull(basicService.getNextBigDecimalId());
   //String
   assertNotNull(basicService.getNextStringId());
}

Strategy Base Service

아이디 생성을 위한 룰을 등록하고 룰에 맞는 아이디를 생성할 수 있도록 지원하는 서비스이다.

위의 Basic Service에서 추가적으로 Strategy 정보 설정을 추가하여 사용 할 수 있다.

단, 이 서비스는 String 타입의 ID만을 제공한다.

DB Schema
CREATE TABLE idttest(
  table_name varchar(16) NOT NULL, 
  next_id DECIMAL(30) NOT NULL,
  PRIMARY KEY (table_name)
);
INSERT INTO idttest VALUES('test','0');
-- ID Generation 서비스를 쓰고자 하는 시스템에서 미리 생성해야 할 DB Schema 정보이다.
Configuration
<bean name="Ids-TestWithGenerationStrategy" class="org.egovframe.rte.fdl.idgnr.impl.EgovTableIdGnrServiceImpl" destroy-method="destroy">
  <property name="dataSource" ref="dataSource"/>
  <property name="strategy" ref="strategy"/>
  <property name="blockSize" value="1"/>
  <property name="table" value="idttest"/>
  <property name="tableName" value="test"/>		
</bean>	

<bean name="strategy" class="org.egovframe.rte.fdl.idgnr.impl.strategy.EgovIdGnrStrategyImpl">
  <property name="prefix" value="TEST-"/>
  <property name="cipers" value="5"/>
  <property name="fillChar" value="*"/>
</bean>
  • strategy
    • 아래에 정의된 MixPrefix 의 bean name 설정
  • prefix
    • 아이디의 앞에 고정적으로 붙이고자 하는 설정값 지정
  • cipers
    • prefix를 제외한 아이디의 길이 지정
  • fillChar
    • 0을 대신하여 표현되는 문자
Sample Source
@Resource(name="Ids-TestWithGenerationStrategy")
private EgovIdGnrService idsTestWithGenerationStrategy;
 
@Test
public void testIdGenStrategy() throws Exception {
 
   initializeNextLongId("test", 1);
 
   // prefix : TEST-, cipers : 5, fillChar :*)
   for (int i = 0; i < 5; i++)
      assertEquals("TEST-****" + (i + 1), idsTestWithGenerationStrategy.getNextStringId());
}

2.15 - Property

Property는 시스템 설정 정보를 외부에서 관리하여 유연성과 확장성을 높이는 기능으로, 동적 갱신이 가능한 Property Service와 정적 설정인 Property Source를 제공한다.

Property

Property는 시스템의 설치 환경에 관련된 정보나, 잦은 정보의 변경이 요구되는 경우 외부에서 그 정보를 관리하게 함으로써 시스템의 유연성을 높이기 위해서 제공하는 것으로 Property Service와 Property Source를 제공하고 있다. Property Service와 Property Source는 각각의 특성과 용도에 따라 시스템의 설정 정보를 관리한다. 이와 같은 기능을 통해 전자정부프레임워크는 시스템의 유연성과 확장성을 높여준다.

  • Property Service
  • 특징: 코드 상에서 key를 통해 해당 값을 가져오는 방식으로, 외부 파일이 변경될 경우 이를 반영하여 값을 갱신할 수 있다.
  • 장점: 시스템 운영 중에도 설정 값을 동적으로 변경할 수 있어 유연성이 높다.
  • 사용 예시: 데이터베이스 연결 정보, API 키 등 자주 변경될 수 있는 설정 값 관리.
  • Property Source
  • 특징: XML 또는 코드 상에서 key를 통해 값을 가져올 수 있지만, 외부 파일이 변경되어도 즉시 반영되지 않는다.
  • 단점: 설정 값 변경 시 시스템을 재시작해야 변경 사항이 반영됩니다.
  • 사용 예시: 상대적으로 변경이 적은 설정 값 관리, 예를 들어 애플리케이션의 기본 설정 값.
  • Property 파일 사용 방법:
  • 별도의 Property 파일을 만들어 Spring Bean 설정 파일에 파일의 위치를 입력하여 사용할 수 있다.
  • 외부 설정 파일에 기재된 프로퍼티 내용은 어플리케이션 운영 중에 추가 및 변경이 가능하다.

2.16 - Property Service

Property Service는 시스템 설정 정보를 외부 파일이나 Spring Bean 설정 파일에서 관리하여 유연성을 제공하는 서비스이다. Bean 설정 파일을 사용하여 간단하게 설정할 수 있지만, 변경 시 어플리케이션 재시작이 필요하다. 외부 설정 파일을 사용하면 운영 중에도 설정 정보를 수정할 수 있으며, 실시간 갱신이 가능하다.

Property Service

개요

Property Service 는 시스템의 설치 환경에 관련된 정보나, 잦은 정보의 변경이 요구되는 경우 외부에서 그 정보를 관리하게 함으로써 시스템의 유연성을 높이기 위해서 제공하는 것으로 Spring Bean 설정 파일에 관리하고자 하는 정보를 입력(Bean 설정 파일 사용) 하거나 외부 파일에 정보 입력 후에 Bean 설정 파일에서 그 파일 위치를 입력하여 이용(외부 설정 파일 사용)할 수 있다.

Bean 설정 파일 사용

간단하게 설정하고자 할 때 사용할 수 있는 방법으로 별도의 외부파일을 두지 않고 Spring Bean 설정 파일을 이용할 수 있다. 하지만 어플리케이션 운영 중에 Property 정보 변경은 불가능 하고 변경처리 시 어플리케이션을 재기동해야 한다. 사용하기 위해서는 bean property의 Name에 properties라고 입력하고 map entry의 key에 관리하고자 하는 키, value에 관리하고자 하는 값을 입력하여 설정한다.

Configuration

<bean name="propertyService" class="egovframework.rte.fdl.property.impl.EgovPropertyServiceImpl" 
        destroy-method="destroy">
    <property name="properties">
        <map>
        <entry key="AAAA" value="1234"/>
        </map>
    </property>			
</bean>
  • propertyService Bean을 추가할 경우 messageSource Bean도 함께 설정해 주어야 한다.
  • 아래의 코드를 추가 설정한다.

messageSource

<bean id="messageSource"
	class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
	<property name="basenames">
		<list>
			<value>classpath:/message/message-common</value>
			<value>classpath:/egovframework/rte/fdl/idgnr/messages/idgnr</value>
			<value>classpath:/egovframework/rte/fdl/property/messages/properties</value>
		</list>
	</property>
	<property name="cacheSeconds">
		<value>60</value>
	</property>
</bean>

Sample Source

@Resource(name="propertyService")
protected EgovPropertyService propertyService ;
 
@Test
public void testPropertiesService() throws Exception {
   assertEquals("1234",propertyService.getString("AAAA"));
}

제공유형별 설정/사용 방법

제공유형설정 방법사용 방법
Stringkey=“A” value=“ABC”propertyService.getString(“A”)
booleankey=“B” value=“true”propertyService.getBoolean(“B”)
intkey=“C” value=“123”propertyService.getInt(“C”)
longkey=“D” value=“123”propertyService.getLong(“D”)
shortkey=“E” value=“123”propertyService.getShort(“E”)
floatkey=“F” value=“123”propertyService.getFloat(“F”)
Vectorkey=“G” value=“123,456”propertyService.getVector(“G”)

외부 설정 파일 사용

별도의 Property 파일을 만들어서 사용하는 방법으로 Spring Bean 설정 파일에는 파일의 위치를 입력하여 이용할 수 있다. 외부 설정 파일에 기재된 프로퍼티 내용은 어플리케이션 운영 중에 추가 및 변경 가능하다.

Configuration

<bean name="propertyService" class="egovframework.rte.fdl.property.impl.EgovPropertyServiceImpl" 
        destroy-method="destroy">
    <property name="extFileName">
        <set>
        <map>
            <entry key="encoding" value="UTF-8"/>
            <entry key="filename" value="file:./src/**/refresh-resource.properties"/>
        </map>
        <value>classpath*:properties/resource.properties</value>
        </set>
    </property>			
</bean>
  • MAP을 이용해서 encoding 정보를 입력하는 방법과 파일 위치만을 기재하는 방법 두가지 설정방법 있음

properties 파일

AAAA=1234

Sample Source

@Resource(name="propertyService")
protected EgovPropertyService propertyService ;
 
@Test
public void testPropertiesService() throws Exception {
   assertEquals("1234",propertyService.getString("AAAA"));
}

실시간 갱신 방법

  1. 외부파일에 기재된 property 내용을 수정한다.
  2. propertyService.refreshPropertyFiles()를 호출한다.

2.17 - Property Source

Property Source는 Spring에서 properties 파일이나 DB 테이블에서 key-value 형식의 설정 값을 가져올 수 있도록 하는 기능이다. Property-placeholder는 XML 설정 파일에서 ${}를 사용해 외부 설정 값을 참조하며, Spring 3.1 이후에는 PropertySourcesPlaceholderConfigurer가 사용된다. DB PropertySource는 DB 테이블에서 설정 값을 가져오는 기능을 제공하며, DBPropertySourceInitializer를 통해 WAS 기동 시 설정 값을 로드할 수 있다.

Property Source

개요

Property Source는 property place-holder를 이용하여 xml의 bean설정에서 key값을 통해 가져올 수 있으며 코드상에서는 Environment를 이용하여 해당값을 가져올 수 있다.

기본적으로 properties파일을 통한 기능을 제공하고 있으며 추가적인 설정을 통해 DB의 테이블에서 property값을 가져오는 PropertySource를 제공하고 있다. 또한 사용자가 추가로 PropertySource를 정의할 수 있다.

Property-placeholder와 PropertySource

Property-placeholder

bean을 정의할 때 ${…}의 내용을 property placeholder를 이용하여 대체할 수 있었다. 해당 코드는 다음과 같다.

<context:property-placeholder location="com/bank/config/datasource.properties"/>
 
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClass" value="${database.driver}"/>
    <property name="jdbcUrl" value="${database.url}"/>
    <property name="username" value="${database.username}"/>
    <property name="password" value="${database.password}"/>
</bean>

Spring 3.1이전에는 <context:property-placeholder>를 정의하면 PropertyPlaceholderConfigurer를 사용하였다. 그러나 Spring 3.1이후부터 PropertySourcesPlaceholderConfigurer를 내부에서 사용하고 있으며 위에서 ${database.*}값을 datasource.properties에서 찾지 못하면 Environment의 Property를 사용하도록 하고 있다. PropertySource는 Environment를 통해 접근 가능하다. 즉, 사용자가 정의한 PropertySource 또한 Spring 3.1부터 property-placeholder를 통해 사용할 수 있는 것이다.

사용자 정의 PropertySource

Spring 3.1에서는 사용자가 직접 PropertySource를 정의할 수 있는 방법을 제시한다. ApplicationContextInitializer인터페이스와 web.xml에 contextInitializerClasses서블릿 컨텍스트 파라미터, Environment를 이용하여 정의가 가능하다.

ApplicationContextInitializer인터페이스를 구현하여 ApplicationContext초기화 로직을 직접 등록할 수 있으며 이 때 contextInitializerClasses 서블릿 컨텍스트 파라미터에 이 구현클래스를 등록한다.

web.xml의 정의 예이다.

<context-param>
    <param-name>contextInitializerClasses</param-name>
    <param-value>com.bank.MyInitializer</param-value>
</context-param>

이 때 ApplicationContextInitializer 구현 클래스인 MyInitializer 예이다.

public class MyInitializer implements ApplicationContextInitializer<ConfigurableWebApplicationContext> {
    public void initialize(ConfigurableWebApplicationContext ctx) {
        PropertySource ps = new MyPropertySource();
        ctx.getEnvironment().getPropertySources().addFirst(ps);
        // perform any other initialization of the context ...
    }
}

위와 같이 등록하면 ApplicationContext가 로딩되거나 refresh되는 시점에 ApplicationContextInitializer구현체가 동작한다. ApplicationContextInitializer구현체의 initialize메소드에서 사용자가 정의한 PropertySource(PropertySource를 상속)를 Environment내부의 propertySources에 (getPropertySources함수를 이용하여) 추가한다.

사용자 정의 PropertySource를 등록하면 Environment를 통해 직접 Property를 가져오는 방법이나 Property-placeholder를 통해서 모두 해당 Property를 가져올 수 있다.

DB PropertySource

전자정부 3.0에서는 DB의 테이블에서 Property값을 가져오는 DBPropertySource를 제공하고 있다.

DB PropertySource이용을 위한 설정

DB기반의 PropertySource를 적용하기 위해서는 다음과 같이 설정한다.

  • DB TABLE과 데이터 생성
    • 칼럼명을 PKEY, PVALUE로 갖는 DB TABLE을 생성한다.
  • BEAN 정의
    • dataSource와 dbPropertySource를 빈으로 정의한 xml을 설정한다.
  • web.xml설정
    • web.xml에 위에서 정의한 xml path와 DBPropertySourceInitializer를 설정한다.

DB설정

WAS가 기동될 때 DB를 연결하여 테이블에서 property값들을 가져올 수 있도록 하는 xml을 설정한다. DB property값을 담을 table을 만든다. 이 때, 칼럼명은 PKEY, PVALUE로 만들도록 한다.

CREATE TABLE PROPERTY (
  PKEY VARCHAR(20) NOT NULL  PRIMARY KEY ,
  PVALUE VARCHAR(20) NOT NULL
);
 
commit;
INSERT INTO PROPERTY (PKEY, PVALUE) VALUES ('egov.test.sample01', 'db-property-sample01');
INSERT INTO PROPERTY (PKEY, PVALUE) VALUES ('egov.test.sample02', 'db-property-sample02');
 
...
 
commit;

다음은 db설정 xml의 예이다.

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xmlns:p="http://www.springframework.org/schema/p"
		xmlns:jdbc="http://www.springframework.org/schema/jdbc"
		xmlns:context="http://www.springframework.org/schema/context"
		xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
			http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-3.2.xsd">
 
	<jdbc:embedded-database id="dataSource" type="HSQL">
		<jdbc:script location="classpath:db/ddl.sql" />
		<jdbc:script location="classpath:db/dml.sql" />
	</jdbc:embedded-database>
 
	<bean id="dbPropertySource" class="egovframework.rte.fdl.property.db.DbPropertySource">
		<constructor-arg value="dbPropertySource"/>
		<constructor-arg ref="dataSource"/>
		<constructor-arg value="SELECT PKEY, PVALUE FROM PROPERTY"/>
	</bean>
</beans>

web.xml설정

WAS 기동 시에 DB값을 가져오도록 web.xml에 추가설정이 필요하다. egov에서 제공해주는 DBPropertySourceInitializer를 추가해주고, 앞에서 설정한 xml의 path를 설정해준다.

<context-param>
    <param-name>contextInitializerClasses</param-name>
    <param-value>egovframework.rte.fdl.property.db.initializer.DBPropertySourceInitializer</param-value>
</context-param>
<context-param>
    <param-name>propertySourceConfigLocation</param-name>
    <param-value>classpath:/initial/propertysource-context.xml</param-value>
</context-param>

DB PropertySource접근

xml에서 DBPropertySource이용

xml에서 정의된 PropertySource를 사용하기 위해서 property-placeholder만 설정해주면 된다.

...
<context:property-placeholder/>
 
<!-- 메시지소스빈 설정 -->
<bean id="propertyTest" class="egov.sample.property.PropertyTest">
  <property name="sample01" value="${egov.test.sample01}"/>
  <property name="sample02" value="${egov.test.sample02}"/>
</bean>
...

코드상에서 DBPropertySource이용

코드상에서 PropertySource에 접근하기 위해서는 egov 3.0부터 제공하는 Environment abstraction을 이용한다. 자세한 내용은 Environment를 참조하도록 한다.

참고자료

Spring 3.1 M1: Unified Property Management

2.18 - Environment

표준프레임워크 3.0부터 Spring 3.1의 Environment 인터페이스를 통해 Profile과 Property에 접근할 수 있다.

Environment

개요

표준프레임워크 3.0부터는 (Spring 3.1부터) Environment interface를 제공한다.

Environment는 다음 기능의 접근을 제공한다.

  • Profile
  • Property

Environment는 ApplicationContext를 통해서 접근이 가능하며 다음과 같이 가져올 수 있다.

ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();

Profile 접근

Profile은 등록할 bean들이 정의되어있는 논리적인 그룹을 말한다. Bean은 XML또는 Annotation을 통해 정의된 Profile값 중 활성화된 Profile로 할당된다. 이 때 현재 사용하는 Profile을 활성화하는 것이 바로 Environment의 역할이다. 또한 Profile은 default값으로 설정이 되어있어야 한다.

Spring에서 Profile을 활성화 할 때 내부에서 Environment를 쓰고 있으며 활성화하는 방법은 코드상 변환, 명시적 설정, Annotation설정 등이 있다.

Environment를 이용하여 다음과 같이 사용할 경우 Profile이 dev로 정의되어있는 bean들이 활성화된다.

GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
ctx.getEnvironment().setActiveProfiles("dev");
ctx.load("classpath:/com/bank/config/xml/*-config.xml");
ctx.refresh();

혹은 여러 개의 프로파일을 활성화 시킬 수도 있다.

ctx.getEnvironment().setActiveProfiles("profile1", "profile2");

web.xml의 servlet 설정에서 다음과 같이 명시적으로 쓸 수도 있다. 다음의 경우 profile이 production으로 정의되어있는 bean들이 활성화된다.

<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>spring.profiles.active</param-name>
        <param-value>production</param-value>
    </init-param>
</servlet>

Property 접근

Environment abstract에서는 PropertySource의 계층구조를 통해 통합검색 기능을 지원하고 있다.

다음은 Environment를 통해 PropertySource에 접근할 수 있는 예를 보여준다.

ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsFoo = env.containsProperty("foo");
System.out.println("Does my environment contain the 'foo' property? " + containsFoo);

PropertySource에서는 key-value쌍의 속성값에 접근할 수 있도록 기능을 제공하며 기본(default) Environment에는 두 개의 PropertySource객체로 구성되어있다.

  • JVM 시스템 Properties (JVM system properties)
  • 시스템 환경변수 (Systen environment variables)

Environment에서 PropertySource를 Hierarchical(계층구조)의 우선순위대로 검색한다. 위의 default environment에서 JVM 시스템 properties가 시스템 환경변수보다 우선순위가 높으므로 JVM 시스템 Properties에서 검색을 하고 다음 시스템 환경변수에서 Property값을 찾을 것이다.

위의 두 기본 PropertySource외에 사용자 정의 PropertySource를 만들 수 있다.

다음은 사용자가 정의한 MyPropertySource를 Environment의 Hierarchical PropertySource에 추가하기 위한 코드이다.

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

위의 코드에서 MyPropertySource는 가장 높은 우선순위로 Environment에 추가된다. 만약 이전 코드와 같이 “foo”의 Property값을 가져오는 경우, MyPropertySource에 값이 있다면 그 값이 반환될 것이다.

2.19 - Cache Service

전자정부 프레임워크에서 EhCache를 사용한 캐시 서비스를 제공하며, Spring 3.1 이전에는 EhCache의 CacheManager를 직접 사용하고 이후 버전에서는 CacheManager Abstraction으로 캐시 사용을 유연하게 지원한다.

Cache Service

개요

전자정부 프레임워크에서 Cache Service는 EhCache를 선정하여 가이드한다.

Spring 버전 3.1 이전에서는 EhCache에서 제공하는 CacheManager를 직접 사용한다. 3.1 이후 버전에서는 CacheManager Abstraction을 제공함으로써 Cache를 유연하게 사용할 수 있게 되었다. 아래에서는 EhCache의 설명과 Spring 3.1 이전의 EhCache 사용법에 대하여 알아본다.

설명

EhCache를 이용하기 위한 기본 설정 및 기본 사용법에 대해서 설명한다.

Bootstrap Source

Cache를 사용하기 위해서 Cache Manager를 생성하는 방법을 샘플을 통해서 설명한다.

//클래스 패스를 이용하여 설정파일 읽어서 Cache Manager 생성하기.
URL url = getClass().getResource("/ehcache-default.xml");
manager = new CacheManager(url);

위에서 getResource를 통해서 읽어들이는 /ehcache-default.xml 의 파일 내용은 다음과 같다.

Configuration

<ehcache>
<diskStore path="user.dir/second"/>
   <cache name="sampleMem"
           maxElementsInMemory="3"
           eternal="false"
           timeToIdleSeconds="360"
           timeToLiveSeconds="1000"
           overflowToDisk="false"
           diskPersistent="false"  
           memoryStoreEvictionPolicy="LRU">
   </cache>
</ehcache>

Basic Usage Source

위에서 정의한 Cache Manager에서 Cache를 얻어서 기본적인 쓰고 읽고 지우는 것에 대한 샘플은 다음과 같다.

// cache Name을 가지고 cache 얻기
Cache cache = manager.getCache("sampleMem");
 
// 1.Cache에 데이터 입력
cache.put(new Element("key1", "value1"));
 
// 2.Cache로부터 입력한 데이터 읽기
Element value = cache.get("key1");
 
// 3. Cache에서 데이터 삭제
cache.remove("key1");
  • manager.getCache
    • Manager를 이용하여 해당하는 Cache 얻기
  • cache.put
    • Cache에 자료 입력 ( 같은 키 값에 대해서 다른 value를 입력하면 수정처리됨 )
  • cache.get
    • Cache에서 자료 읽기
  • cache.remove
    • Cache에서 자료 삭제.

Cache Algorithm

Cache는 메모리를 이용하는 것을 기본으로 하기에 꼭 필요한 자료만을 관리하도록 보관 사이즈를 지정한다. 보관 사이즈를 넘어설 경우 불필요한 자료부터 삭제처리하는데 필요한 자료에 대한 판단은 알고리즘을 통해서 한다. 지원되는 알고리즘은 LRU, FIFO, LFU 세가지 이고 각각에 대한 설정및 사용법은 아래와 같다.

LRU Algorithm

최근에 이용한 것을 남기는 알고리즘으로 설정은 아래와 같이 한다.

Configuration

<cache name="sampleMem"
        maxElementsInMemory="3"
        ...
        memoryStoreEvictionPolicy="LRU">

위의 설정에서 최대 보관 엔트리 갯수(maxElementsInMemory)는 3, 알고리즘은 LRU 로 설정된 것을 확인할 수 있다. 이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.

Sample Source

   Cache cache = manager.getCache("sampleMem");
   ...
   cache.get("key2");
   cache.get("key2");
   cache.get("key1");
   cache.get("key1");
   cache.get("key3");
 
   // 4. Put New element in cache.
   cache.put(new Element("key4", "value4"));    	
 
   // 5. get key2 but can't key2 
   assertNull("Can't get key2",cache.get("key2"));

FIFO Algorithm

먼저 입력된것을 제거하는 알고리즘으로 설정은 아래와 같이 한다.

Configuration

<cache name="sampleMemFIFO"
        maxElementsInMemory="3"
        ...
        memoryStoreEvictionPolicy="FIFO">

위의 설정에서 최대 보관 엔트리 갯수(maxElementsInMemory)는 3, 알고리즘은 FIFO 로 설정된 것을 확인할 수 있다. 이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.

Sample Source

Cache cache = manager.getCache("sampleMemFIFO");
cache.put(new Element("key1", "value1")); 
cache.put(new Element("key2", "value2")); 
cache.put(new Element("key3", "value3")); 
...
cache.get("key2");
cache.get("key2");
cache.get("key1");
cache.get("key1");
cache.get("key3");

// 4. Put New element in cache.
cache.put(new Element("key4", "value4"));    	

// 5. get key1 but can't key1 
assertNull("Can't get key1",cache.get("key1"));

LFU Algorithm

가장 적게 이용된 것을 제거하는 알고리즘으로 설정은 아래와 같이 한다.

Configuration

<cache name="sampleMemLFU"
        maxElementsInMemory="3"
        ...
        memoryStoreEvictionPolicy="LFU">

위의 설정에서 최대 보관 엔트리 갯수(maxElementsInMemory)는 3, 알고리즘은 LFU 로 설정된 것을 확인할 수 있다. 이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.

Sample Source

Cache cache = manager.getCache("sampleMemLFU");
cache.put(new Element("key1", "value1")); 
cache.put(new Element("key2", "value2")); 
cache.put(new Element("key3", "value3")); 
...
cache.get("key2");
cache.get("key2");
cache.get("key1");
cache.get("key1");
cache.get("key3");

// 4. Put New element in cache.
cache.put(new Element("key4", "value4"));    	

// 5. get key2 but can't key3 
assertNull("Can't get key3",cache.get("key3"));

Cache Size & Device

Cache에서 관리해야 하는 정보의 사이즈 설정 및 저장 디바이스 관련 설정을 할 수 있다.

Cache Device

디바이스 관련 세팅은 메모리 관리 cache의 디스크 관리로의 이동관련 설정으로 메모리 관리 오브젝트 수가 넘었을때 Disk로 이동여부, flush 호출시 파일로 저장 여부에 대한 설정이 있다. 이에 대한 설정 예시는 다음과 같다.

Configuration

<cache name="sampleDisk"
       overflowToDisk="true"
       diskPersistent="true"  
       ...>
</cache>
  • overflowToDisk
    • 메모리 관리 오브젝트 넘었을때 Disk로 이동 여부 ( true,false )
  • diskPersistent
    • flush시에 파일로 저장 여부 ( trun , false )

이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.

Sample Source

Ehcache cache = manager.getCache("sampleDisk");

// 1. Put a content in Cache.
cache.put(new Element("key1", "value1"));

// 2. Get that item from Cache.
Element value = cache.get("key1");
assertEquals("value1", value.getValue().toString());

// 3. flush 를 한다.
cache.flush();		
File dataFile = new File(manager.getDiskStorePath()+ File.separator + "sampleDisk.data");
// 4. 파일로 저장 확인
assertTrue("File exists", dataFile.exists());

위의 샘플에서 flush 수행시 파일로 저장되는 것을 확인 할 수 있고, 메모리 관리 오브젝트 Disk이동 여부는 아래의 Cache Size의 예에서 확인한다.

Cache Size

사이즈 관련 설정은 메모리에서 관리해야 할 최대 오브젝트 수, 디스크에서 관리하는 최대 오브젝트 수에 대한 설정이 있다. 이에 대한 설정 예시는 다음과 같다.

Configuration

<cache name="sampleDisk"
       maxElementsInMemory="3"
       maxElementsOnDisk="2"
       overflowToDisk="true"
       ...>
</cache>
  • maxElementsInMemory
    • 메모리에서 관리하는 최대 오브젝트 수
  • maxElementsOnDisk
    • 디스크에서 관리하는 최대 오브젝트 수

이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.

Sample Source

Cache cache = manager.getCache("sampleDisk");

// 1. Put 3 contents in Cache.
cache.put(new Element("key1", "value1"));
cache.put(new Element("key2", "value2"));
cache.put(new Element("key3", "value3"));

// 2. Put Fourth content in Cache.
cache.put(new Element("key4", "value4"));
assertEquals(3, cache.getMemoryStoreSize()); //Memory에는 세개 유지함
assertEquals(1, cache.getDiskStoreSize());   //Disk로 하나 넘어감.

// 3. Put 5~7 contens in Cache.    
cache.put(new Element("key5", "value5"));
cache.put(new Element("key6", "value6"));
cache.put(new Element("key7", "value7"));

// Disk Max Size에 관계 없이 모두 넘어감
assertEquals(3, cache.getMemoryStoreSize()); //Memory에는 세개 유지함
assertEquals(4, cache.getDiskStoreSize());   //Disk에는 2개를 넘어서서 4개 유지함.

// 메모리것을 디스크로 이동시킴
cache.flush();
// Disk의 Max Size 대로 변경됨.
assertEquals(0, cache.getMemoryStoreSize()); //Memory에는 없어짐.
assertEquals(2, cache.getDiskStoreSize());   //Disk에는 DiskMaxSize 대로 2개만 남김.

위의 예를 보면 cache.put에 의해서는 Disk의 MaxSize에 관계없이 계속 Memory에서 Disk로 넘어가지만 flush를 수행하면 최대 디스크 보관수만을 남기는 것을 확인 할 수 있다.

Distributed Cache

Ehcache는 Distributed Cache를 지원하는 방법으로 RMI,JGROUP,JMS등을 지원한다. 그 중에서 JGROUP와 ActiveMQ를 이용한 JMS 지원 설정 및 사용 방법을 설명한다. 자세한 설명은 Ehcache Documentation 참고.

Using JGroups

JGroups는 multicast 기반의 커뮤니케이션 툴킷으로 그룹을 생성하고 그룹멤버간에 메세지를 주고 받을 수 있도록 지원한다. 관련한 자세한 정보는 사이트참조

Configuration

<cacheManagerPeerProviderFactory class="net.sf.ehcache.distribution.jgroups.JGroupsCacheManagerPeerProviderFactory"
                                    properties="connect=UDP(mcast_addr=224.10.10.10;mcast_port=5555;ip_ttl=32;
                                    mcast_send_buf_size=150000;mcast_recv_buf_size=80000):
                                    PING(timeout=2000;num_initial_members=6):
                                    MERGE2(min_interval=5000;max_interval=10000):
                                    FD_SOCK:VERIFY_SUSPECT(timeout=1500):
                                    pbcast.NAKACK(gc_lag=10;retransmit_timeout=3000):
                                    UNICAST(timeout=5000):
                                    pbcast.STABLE(desired_avg_gossip=20000):
                                    FRAG:
                                    pbcast.GMS(join_timeout=5000;join_retry_timeout=2000;shun=false;print_local_addr=false)"
                                    propertySeparator="::"/>
    <cache name="cacheSync"
            maxElementsInMemory="1000"
            eternal="false"
            timeToIdleSeconds="1000"
            timeToLiveSeconds="1000"
            overflowToDisk="false">
    <cacheEventListenerFactory class="net.sf.ehcache.distribution.jgroups.JGroupsCacheReplicatorFactory"
                                properties="replicateAsynchronously=false, replicatePuts=true,
                                            replicateUpdates=true, replicateUpdatesViaCopy=false,
                                            replicateRemovals=true"/>
</cache>

이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.

Sample Source

// 위의 설정파일을 정보 읽어오기
URL url = this.getClass().getResource("/ehcache-distributed-jgroups.xml");
// 두개의 Cache Manager 생성
manager1 = new CacheManager(url);
manager2 = new CacheManager(url);

for (int i = 0; i < 10 ; i++) {
    manager1.getEhcache(CACHE_SYNC).put(new Element(new Integer(i), "value"));
}		
// 리플리케이션을 위한 시간 필요함.     
Thread.currentThread().sleep(100);
// 리플리케이션이 되어 manager2에도 동일한 Cache 정보 입력됨 확인.
assertTrue(manager1.getEhcache(CACHE_SYNC).getKeys().size() == manager2.getEhcache(CACHE_SYNC).getKeys().size() );

Using ActiveMQ

ActiveMQ는 JMS 메세징 서비스를 제공하는 오픈 소스이다. 관련한 자세한 정보는 사이트 참조

Configuration

<cacheManagerPeerProviderFactory
        class="net.sf.ehcache.distribution.jms.JMSCacheManagerPeerProviderFactory"
        properties="initialContextFactoryName=egovframework.rte.fdl.cache.distribute.TestActiveMQInitialContextFactory,
            providerURL=tcp://localhost:61616,
            replicationTopicConnectionFactoryBindingName=topicConnectionFactory,
            getQueueConnectionFactoryBindingName=queueConnectionFactory,
            replicationTopicBindingName=ehcache,
            getQueueBindingName=ehcacheGetQueue"
        propertySeparator=","
        />

    <cache name="CacheAsync"
            maxElementsInMemory="1000"
            eternal="false"
            timeToIdleSeconds="1000"
            timeToLiveSeconds="1000"
            overflowToDisk="false">

        <cacheEventListenerFactory class="net.sf.ehcache.distribution.jms.JMSCacheReplicatorFactory"
                                properties="replicateAsynchronously=true, 
                                            replicatePuts=true,
                                            replicateUpdates=true,
                                            replicateUpdatesViaCopy=true,
                                            replicateRemovals=true,
                                            asynchronousReplicationIntervalMillis=1000"
                                propertySeparator=","/>
    </cache>

이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.

Sample Source

URL url = this.getClass().getResource("/ehcache-distributed-activemq.xml");
manager1 = new CacheManager(url);
manager2 = new CacheManager(url);

cacheName = "CacheAsync";
for (int i = 0; i < 10 ; i++) {
    manager1.getEhcache("CacheAsync").put(new Element(new Integer(i), "value"));
}		

// replication 되는데 일정 시간이 필요함.
Thread.currentThread().sleep(1000);

assertTrue(manager1.getEhcache("CacheAsync").getKeys().size() == manager2.getEhcache("CacheAsync").getKeys().size() );

Spring Integration

Spring 3.1 이전 버전에서 Ehcache를 이용하는 방법에 대해서 설정 및 설정을 이용한 기본 Cache 서비스에 대해서 설명한다.

Configuration - Spring Application Context

<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
    <property name="cacheManager">
        <bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
        <property name="configLocation" value="classpath:ehcache-default.xml"/>
        </bean>
    </property>    
</bean>

위와 같이 설정하면 Manager를 통하지 않고서 cache를 얻을 수 있다. ConfigLocation에 정의된 ehcache-default.xml은 Cache Basic의 Configuration에 설명한 설정파일과 동일한 것이다.

Sample Source

@Resource(name="ehcache")
Ehcache gCache ;

// cache Name을 가지고 cache 찾기
Ehcache cache = gCache.getCacheManager().getCache("sampleMem");    	

cache.put(new Element("key1", "value1"));
Element value = cache.get("key1");

위의 예를 보면 ehcache를 이용하여 Ehcache를 가지고 오고 getCacheManager()를 통해서 이름을 통한 cache 정보를 읽어오는 것을 확인할 수 있다. 그 이후에는 위에서 설명한 것과 동일한 방식으로 사용하는 것을 확인 할 수 있다.

참고자료

EhCache

2.20 - EhCache

전자정부 프레임워크에서 EhCache를 사용한 캐시 서비스를 제공하며, Spring 3.1 이전에는 EhCache의 CacheManager를 직접 사용하고 이후 버전에서는 CacheManager Abstraction으로 캐시 사용을 유연하게 지원한다.

EhCache

개요

전자정부 프레임워크에서 Cache Service는 EhCache를 선정하여 가이드한다.

Spring 버전 3.1 이전에서는 EhCache에서 제공하는 CacheManager를 직접 사용한다. 3.1 이후 버전에서는 CacheManager Abstraction을 제공함으로써 Cache를 유연하게 사용할 수 있게 되었다. 아래에서는 EhCache의 설명과 Spring 3.1이전의 EhCache 사용법에 대하여 알아본다.

설명

EhCache를 이용하기 위한 기본 설정 및 기본 사용법에 대해서 설명한다.

Bootstrap Source

Cache를 사용하기 위해서 Cache Manager를 생성하는 방법을 샘플을 통해서 설명한다.

//클래스 패스를 이용하여 설정파일 읽어서 Cache Manager 생성하기.
URL url = getClass().getResource("/ehcache-default.xml");
manager = new CacheManager(url);

위에서 getResource를 통해서 읽어들이는 /ehcache-default.xml 의 파일 내용은 다음과 같다.

Configuration

<ehcache>
<diskStore path="user.dir/second"/>
   <cache name="sampleMem"
           maxElementsInMemory="3"
           eternal="false"
           timeToIdleSeconds="360"
           timeToLiveSeconds="1000"
           overflowToDisk="false"
           diskPersistent="false"  
           memoryStoreEvictionPolicy="LRU">
   </cache>
</ehcache>

Basic Usage Source

위에서 정의한 Cache Manager에서 Cache를 얻어서 기본적인 쓰고 읽고 지우는 것에 대한 샘플은 다음과 같다.

// cache Name을 가지고 cache 얻기
Cache cache = manager.getCache("sampleMem");
 
// 1.Cache에 데이터 입력
cache.put(new Element("key1", "value1"));
 
// 2.Cache로부터 입력한 데이터 읽기
Element value = cache.get("key1");
 
// 3. Cache에서 데이터 삭제
cache.remove("key1");
  • manager.getCache
    • Manager를 이용하여 해당하는 Cache 얻기
  • cache.put
    • Cache에 자료 입력 ( 같은 키 값에 대해서 다른 value를 입력하면 수정처리됨 )
  • cache.get
    • Cache에서 자료 읽기
  • cache.remove
    • Cache에서 자료 삭제

Cache Algorithm

Cache는 메모리를 이용하는 것을 기본으로 하기에 꼭 필요한 자료만을 관리하도록 보관 사이즈를 지정한다. 보관 사이즈를 넘어설 경우 불필요한 자료부터 삭제처리하는데, 필요한 자료에 대한 판단은 알고리즘을 통해서 한다. 지원되는 알고리즘은 LRU, FIFO, LFU 세 가지이고 각각에 대한 설정 및 사용법은 아래와 같다.

LRU Algorithm

최근에 이용한 것을 남기는 알고리즘으로 설정은 아래와 같이 한다.

Configuration

<cache name="sampleMem"
        maxElementsInMemory="3"
        ...
        memoryStoreEvictionPolicy="LRU">

위의 설정에서 최대 보관 엔트리 갯수(maxElementsInMemory)는 3, 알고리즘은 LRU 로 설정된 것을 확인할 수 있다. 이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.

Sample Source

Cache cache = manager.getCache("sampleMem");
...
cache.get("key2");
cache.get("key2");
cache.get("key1");
cache.get("key1");
cache.get("key3");

// 4. Put New element in cache.
cache.put(new Element("key4", "value4"));    	

// 5. get key2 but can't key2 
assertNull("Can't get key2",cache.get("key2"));

FIFO Algorithm

먼저 입력된것을 제거하는 알고리즘으로 설정은 아래와 같이 한다.

Configuration

<cache name="sampleMemFIFO"
        maxElementsInMemory="3"
        ...
        memoryStoreEvictionPolicy="FIFO">

위의 설정에서 최대 보관 엔트리 갯수(maxElementsInMemory)는 3, 알고리즘은 FIFO 로 설정된 것을 확인할 수 있다. 이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.

Sample Source

   Cache cache = manager.getCache("sampleMemFIFO");
   cache.put(new Element("key1", "value1")); 
   cache.put(new Element("key2", "value2")); 
   cache.put(new Element("key3", "value3")); 
   ...
   cache.get("key2");
   cache.get("key2");
   cache.get("key1");
   cache.get("key1");
   cache.get("key3");
 
   // 4. Put New element in cache.
   cache.put(new Element("key4", "value4"));    	
 
   // 5. get key1 but can't key1 
   assertNull("Can't get key1",cache.get("key1"));

LFU Algorithm

가장 적게 이용된 것을 제거하는 알고리즘으로 설정은 아래와 같이 한다.

Configuration

   <cache name="sampleMemLFU"
           maxElementsInMemory="3"
           ...
           memoryStoreEvictionPolicy="LFU">

위의 설정에서 최대 보관 엔트리 갯수(maxElementsInMemory)는 3, 알고리즘은 LFU 로 설정된 것을 확인할 수 있다. 이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.

Sample Source

Cache cache = manager.getCache("sampleMemLFU");
cache.put(new Element("key1", "value1")); 
cache.put(new Element("key2", "value2")); 
cache.put(new Element("key3", "value3")); 
...
cache.get("key2");
cache.get("key2");
cache.get("key1");
cache.get("key1");
cache.get("key3");

// 4. Put New element in cache.
cache.put(new Element("key4", "value4"));    	

// 5. get key2 but can't key3 
assertNull("Can't get key3",cache.get("key3"));

Cache Size & Device

Cache에서 관리해야 하는 정보의 사이즈 설정 및 저장 디바이스 관련 설정을 할 수 있다.

Cache Device

디바이스 관련 설정은 메모리 관리 cache의 디스크 관리로의 이동관련 설정으로 메모리 관리 오브젝트 수가 넘었을때 Disk로 이동여부, flush 호출시 파일로 저장 여부에 대한 설정이 있다. 이에 대한 설정 예시는 다음과 같다.

Configuration

<cache name="sampleDisk"
       overflowToDisk="true"
       diskPersistent="true"  
       ...>
</cache>
  • overflowToDisk
    • 메모리 관리 오브젝트 넘었을때 Disk로 이동 여부 (true, false)
  • diskPersistent
    • flush시에 파일로 저장 여부 (true, false)

이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.

Sample Source

Ehcache cache = manager.getCache("sampleDisk");

// 1. Put a content in Cache.
cache.put(new Element("key1", "value1"));

// 2. Get that item from Cache.
Element value = cache.get("key1");
assertEquals("value1", value.getValue().toString());

// 3. flush 를 한다.
cache.flush();		
File dataFile = new File(manager.getDiskStorePath()+ File.separator + "sampleDisk.data");
// 4. 파일로 저장 확인
assertTrue("File exists", dataFile.exists());

위의 샘플에서 flush 수행시 파일로 저장되는 것을 확인 할 수 있고, 메모리 관리 오브젝트 Disk이동 여부는 아래의 Cache Size의 예에서 확인한다.

Cache Size

사이즈 관련 설정은 메모리에서 관리해야 할 최대 오브젝트 수, 디스크에서 관리하는 최대 오브젝트 수에 대한 설정이 있다. 이에 대한 설정 예시는 다음과 같다.

Configuration

<cache name="sampleDisk"
       maxElementsInMemory="3"
       maxElementsOnDisk="2"
       overflowToDisk="true"
       ...>
</cache>
  • maxElementsInMemory
    • 메모리에서 관리하는 최대 오브젝트 수
  • maxElementsOnDisk
    • 디스크에서 관리하는 최대 오브젝트 수

이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.

Sample Source

Cache cache = manager.getCache("sampleDisk");

// 1. Put 3 contents in Cache.
cache.put(new Element("key1", "value1"));
cache.put(new Element("key2", "value2"));
cache.put(new Element("key3", "value3"));

// 2. Put Fourth content in Cache.
cache.put(new Element("key4", "value4"));
assertEquals(3, cache.getMemoryStoreSize()); //Memory에는 세개 유지함
assertEquals(1, cache.getDiskStoreSize());   //Disk로 하나 넘어감.

// 3. Put 5~7 contens in Cache.    
cache.put(new Element("key5", "value5"));
cache.put(new Element("key6", "value6"));
cache.put(new Element("key7", "value7"));

// Disk Max Size에 관계 없이 모두 넘어감
assertEquals(3, cache.getMemoryStoreSize()); //Memory에는 세개 유지함
assertEquals(4, cache.getDiskStoreSize());   //Disk에는 2개를 넘어서서 4개 유지함.

// 메모리것을 디스크로 이동시킴
cache.flush();
// Disk의 Max Size 대로 변경됨.
assertEquals(0, cache.getMemoryStoreSize()); //Memory에는 없어짐.
assertEquals(2, cache.getDiskStoreSize());   //Disk에는 DiskMaxSize 대로 2개만 남김.

위의 예를 보면 cache.put에 의해서는 Disk의 MaxSize에 관계없이 계속 Memory에서 Disk로 넘어가지만 flush를 수행하면 최대 디스크 보관수만을 남기는 것을 확인 할 수 있다.

Distributed Cache

Ehcache는 Distributed Cache를 지원하는 방법으로 RMI, JGROUP, JMS등을 지원한다. 그 중에서 JGROUP와 ActiveMQ를 이용한 JMS 지원 설정 및 사용 방법을 설명한다. 자세한 설명은 Ehcache Documentation 참고.

Using JGroups

JGroups는 multicast 기반의 커뮤니케이션 툴킷으로 그룹을 생성하고 그룹멤버간에 메세지를 주고 받을 수 있도록 지원한다. 관련한 자세한 정보는 사이트 참고

Configuration

    <cacheManagerPeerProviderFactory class="net.sf.ehcache.distribution.jgroups.JGroupsCacheManagerPeerProviderFactory"
                                     properties="connect=UDP(mcast_addr=224.10.10.10;mcast_port=5555;ip_ttl=32;
                                     mcast_send_buf_size=150000;mcast_recv_buf_size=80000):
                                     PING(timeout=2000;num_initial_members=6):
                                     MERGE2(min_interval=5000;max_interval=10000):
                                     FD_SOCK:VERIFY_SUSPECT(timeout=1500):
                                     pbcast.NAKACK(gc_lag=10;retransmit_timeout=3000):
                                     UNICAST(timeout=5000):
                                     pbcast.STABLE(desired_avg_gossip=20000):
                                     FRAG:
                                     pbcast.GMS(join_timeout=5000;join_retry_timeout=2000;shun=false;print_local_addr=false)"
                                     propertySeparator="::"/>
    <cache name="cacheSync"
           maxElementsInMemory="1000"
           eternal="false"
           timeToIdleSeconds="1000"
           timeToLiveSeconds="1000"
           overflowToDisk="false">
        <cacheEventListenerFactory class="net.sf.ehcache.distribution.jgroups.JGroupsCacheReplicatorFactory"
                                   properties="replicateAsynchronously=false, replicatePuts=true,
 												replicateUpdates=true, replicateUpdatesViaCopy=false,
 												replicateRemovals=true"/>
    </cache>

이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.

Sample Source

// 위의 설정파일을 정보 읽어오기
URL url = this.getClass().getResource("/ehcache-distributed-jgroups.xml");
// 두개의 Cache Manager 생성
manager1 = new CacheManager(url);
manager2 = new CacheManager(url);

for (int i = 0; i < 10 ; i++) {
    manager1.getEhcache(CACHE_SYNC).put(new Element(new Integer(i), "value"));
}		
// 리플리케이션을 위한 시간 필요함.     
Thread.currentThread().sleep(100);
// 리플리케이션이 되어 manager2에도 동일한 Cache 정보 입력됨 확인.
assertTrue(manager1.getEhcache(CACHE_SYNC).getKeys().size() == manager2.getEhcache(CACHE_SYNC).getKeys().size() );

Using ActiveMQ

ActiveMQ는 JMS 메세징 서비스를 제공하는 오픈 소스이다. 관련한 자세한 정보는 사이트 참조

Configuration

    <cacheManagerPeerProviderFactory
            class="net.sf.ehcache.distribution.jms.JMSCacheManagerPeerProviderFactory"
            properties="initialContextFactoryName=egovframework.rte.fdl.cache.distribute.TestActiveMQInitialContextFactory,
                providerURL=tcp://localhost:61616,
                replicationTopicConnectionFactoryBindingName=topicConnectionFactory,
                getQueueConnectionFactoryBindingName=queueConnectionFactory,
                replicationTopicBindingName=ehcache,
                getQueueBindingName=ehcacheGetQueue"
            propertySeparator=","
            />
 
    <cache name="CacheAsync"
           maxElementsInMemory="1000"
           eternal="false"
           timeToIdleSeconds="1000"
           timeToLiveSeconds="1000"
           overflowToDisk="false">
 
           <cacheEventListenerFactory class="net.sf.ehcache.distribution.jms.JMSCacheReplicatorFactory"
                                   properties="replicateAsynchronously=true, 
                                                replicatePuts=true,
                                                replicateUpdates=true,
                                                replicateUpdatesViaCopy=true,
                                                replicateRemovals=true,
                                                asynchronousReplicationIntervalMillis=1000"
                                    propertySeparator=","/>
    </cache>

이 설정을 이용하여 설정에 대한 결과를 확인하는 소스는 다음과 같다.

Sample Source

URL url = this.getClass().getResource("/ehcache-distributed-activemq.xml");
manager1 = new CacheManager(url);
manager2 = new CacheManager(url);

cacheName = "CacheAsync";
for (int i = 0; i < 10 ; i++) {
    manager1.getEhcache("CacheAsync").put(new Element(new Integer(i), "value"));
}		

// replication 되는데 일정 시간이 필요함.
Thread.currentThread().sleep(1000);

assertTrue(manager1.getEhcache("CacheAsync").getKeys().size() == manager2.getEhcache("CacheAsync").getKeys().size() );

Spring Integration

Spring 3.1 이전 버전에서 Ehcache를 이용하는 방법에 대해서 설정 및 설정을 이용한 기본 Cache 서비스에 대해서 설명한다.

Configuration - Spring Application Context

<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
    <property name="cacheManager">
        <bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
        <property name="configLocation" value="classpath:ehcache-default.xml"/>
        </bean>
    </property>    
</bean>

위와 같이 설정하면 Manager를 통하지 않고서 cache를 얻을 수 있다. ConfigLocation에 정의된 ehcache-default.xml은 Cache Basic의 Configuration에 설명한 설정파일과 동일한 것이다.

Sample Source

@Resource(name="ehcache")
Ehcache gCache ;

// cache Name을 가지고 cache 찾기
Ehcache cache = gCache.getCacheManager().getCache("sampleMem");    	

cache.put(new Element("key1", "value1"));
Element value = cache.get("key1");

위의 예를 보면 ehcache를 이용하여 Ehcache를 가지고 오고 getCacheManager()를 통해서 이름을 통한 cache 정보를 읽어오는 것을 확인할 수 있다. 그 이후에는 위에서 설명한 것과 동일한 방식으로 사용하는 것을 확인 할 수 있다.

참고자료

EhCache

2.21 - Cache Abstraction

Cache를 설정하여 CacheManager를 통해 Cache에 접근하는 방법에 대하여 알아보고, 자바메소드를 Caching하는 @Cacheable에 대하여 알아본다.

Cache Abstraction

개요

Spring 3.1부터 Cache Service는 Cache 추상화(CacheManager Interface)와 Cache 추상화를 Java메소드에 제공할 수 있는 @Cacheable을 제공한다. Cache 추상화는 Spring의 트랜잭션기능과 유사하게 코드의 변화를 최소화하면서 Proxy를 통해 동작하게끔 한다. Cache 구현체가 아닌 Cache추상화만을 제공하며 실제 Cache Data저장소는 EhCache와 ConcurrentMap을 지원한다.

  • Cache Configuration
    • Cache설정을 통하여 어떠한 Cache Data저장소를 쓸 것인지 결정할 수 있다. (EhCache/ConcurrentMap)
  • Cache Manager
    • CacheManager를 통해 설정과 상관없이 동일한 코드로 Cache에 접근할 수 있다.
  • Cache Annotation
    • 메소드의 Cache Annotation을 통해 쉽게 Cache데이터를 저장/삭제할 수 있다.

설명

Cache를 설정하여 CacheManager를 통해 Cache에 접근하는 방법에 대하여 알아보고, 자바메소드를 Caching하는 @Cacheable에 대하여 알아본다.

Cache Configuration

EhCache

Spring에서는 EhCache를 지원하는 CacheManager로써 EhCacheCacheManager를 제공한다. <EhCache 설정>

<cache:annotation-driven cache-manager="cacheManager" />
<!-- EhCache를 저장소로 사용하는 Cache Manager -->
 <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
<property name="cacheManager" ref="ehcache"></property>
</bean>
<!-- Ehcache library setup -->
<bean id="ehcache"
class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
p:config-location="classpath:springframework/cache/ehcache/ehcache.xml" />

Ehcache.xml에서 defaultCache저장소와 추가 Cache저장소의 설정을 한다. EhCache는 비록 ConcurrentMap보다 속도는 느리지만 Cache관리기능 측면에서 유용하여 EhCache를 추천한다.

  • 프로젝트에서는 Cache의 사용에 대하여 개발자의 가이드가 필요하다. 업무별/서비스별 Cache저장소를 분리하여 Cache데이터를 관리하도록 하며, 적용케이스에 대하여 가이드 하여야 한다.

<ehcache.xml>

<ehcache> 
<defaultCache 
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="true"
            diskSpoolBufferSizeMB="30"
            maxElementsOnDisk="10000000"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"
            statistics="false"
            />
<cache name="ehcache"
           >

ConcurrentMap

Spring에서는 ConcurrentMap을 지원하는 SimpleCacheManager를 제공한다.

<CacheManager설정>

<cache:annotation-driven cache-manager="cacheManager"/>
 <!-- ConcurrentMap을 저장소로 사용하는 Cache Manager -->
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager"> 
   <property name="caches"> 
      <set> 
         <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/>    
         <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="books"/>   
      </set> 
   </property> 
</bean>

Cache Annotation을 위한 annotation-driven 설정

Cache Annotation으로 Cache를 적용하기 위해서는 반드시 <cache:annotatioin-driven>을 붙여주어야한다. 이 네임스페이스는 AOP를 사용해서 캐시 기능을 다양한 방법으로 설정할 수 있는 옵션을 제공한다. 이 설정은 Transaction에 사용되는 <tx:annotation-driven>과 비슷하다.

속성기본값설명
cache-managercacheManager사용할 CacheManager의 이름. CacheManager의 Bean id가 cacheManager가 아닐 경우, 설정해야한다.
modeproxy스프링 AOP를 사용하는 설정이며, “aspect”를 사용할 수도 있다.
proxy-target-classfalsefalse인 경우, JDK의 인터페이스 기반 프록시를 사용한다. true를 사용하면 클래스 기반 프록시를 사용한다.
orderOrdered.LOWEST_PRECEDENCE@Cacheable/@CacheEvict메소드의 Cache advice가 적용되는 순서
  • <cache:annotation-driven/>은 오로지 이것이 정의된 동일 ApplicationContext안의 bean에서 @Cacheable과 @CacheEvict을 찾는다. 즉, <cache:annotation-driven>을 DispatcherServlet을 위한 WebApplicationContext에 선언했을 때 @Cacheable/@CacheEvict는 controller내부에서만 식별된다.

CacheManager를 통한 Cache접근

ehCache, concurrentMap의 설정과 상관없이 코드상으로 동일한 CacheManager인터페이스로 bean을 주입받아 사용할 수 있다.

<CacheManager 의 사용>

import org.springframework.cache.CacheManager;
@Autowired 
private CacheManager cacheManager;
Cache cache = cacheManager.getCache("cache명");

@Cacheable

자바 메소드에 @Cacheable을 설정함으로써 Caching할 수 있다. 타겟 메소드가 호출되었을 때, 캐시에 해당 메서드가 이미 동일한 인자로 있는지 확인하고, 만약 있다면 메소드를 호출하지 않고 캐시해둔 결과를 Proxy에서 반환하게 된다.

Java Method에 적용가능한 Cache Annotation은 다음과 같다.

  • @Cacheable
    • Cache에 메소드 데이터를 생성한다.
  • @CacheEvict
    • Cache에 메소드 데이터를 삭제한다.

@Cacheable 사용방법

별다른 조건 없이 호출되는 모든 인자를 caching하고자 할 때는 아래와 같이 cache명만 쓰면 된다. 이 코드는 Cache이름이 “books”인 cache저장소를 사용하였다. 메소드가 호출될 때 매번 “books”에 Cache데이터를 확인하고 이미 실행된 적이 있는지를 확인한다. 만약 “books”에 데이터가 있으면 그 값을 반환하게 된다.

기본
@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

Cache되는 저장소를 여러 개 정의할 수도 있다. 아래의 코드에서는 findBook메소드 호출 시 “books”와 “isbns” 두 군데에 캐시데이터가 저장된다.

<여러 cache 저장소에 caching>

@Cacheable({ "books", "isbns" })
public Book findBook(ISBN isbn) {...}
Cache Abstraction Key생성

Cache는 키-값으로 저장되며, 캐시된 메소드를 호출시마다 키를 통해 값을 가져오므로 캐시를 찾을 수 있는 키가 생성되어야 한다. 별도의 커스텀 키가 정의되지 않으면 default로 다음과 같은 알고리즘 기반의 KeyGenerator를 사용하여 Key를 생성한다.

  • 매개변수가 아무것도 없으면 0을 반환한다.
  • 매개변수가 하나면, 그 인스턴스를 반환한다.
  • 매개변수가 둘 이상이면, 모든 매개변수의 Hash로 계산된 키를 반환한다.

이 외에 다른 기본키를 생성하려면 org.springframework.cache.KeyGenerator 인터페이스를 구현하면 된다.

Custom key 추가

@Cacheable을 적용한 메소드의 인자가 여러개일 때 Key로 사용할 것을 SpEL로 명시할 수 있다.

@Cacheable(value="books", key="#isbn"
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
 
 
@Cacheable(value="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
 
 
@Cacheable(value="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
Conditional 추가

Conditional을 주어서 그 값이 true이면 caching을 하고, false이면 caching을 하지 않기 때문에 호출 시 매번 메소드 내부가 실행된다. Conditional에서는 SpEL사용이 가능하며 Condition과 Unless를 쓸 수 있다. condition과는 달리 unless는 메소드의 결과값 반환 시점에 결과값을 확인하여 caching여부를 결정하게 된다.

@Cacheable(value="book", condition="#name.length < 32")
public Book findBook(String name)
@Cacheable(value="book", condition="#name.length < 32", unless="#result.hardback")
public Book findBook(String name)

Conditional에 쓰는 SpEL의 설명은 다음과 같다.

명칭위치설명예제
methodNameroot객체호출되는 메소드명#root.methodName
methodroot객체호출되는 메소드#root.method.name
targetroot객체호출되는 타겟 오브젝트#root.target
targetClassroot객체호출되는 타겟 클래스#root.targetClass
argsroot객체타겟을 호출 시 사용되는 인자들(배열)#root.args[0]
cachesroot객체현재 메소드가 실행되는 캐시들의 집합#root.caches[0].name
argument name평가 context메소드 인자명을 사용할 수 없을 때 대신 a<#arg>로 대체하여 사용할 수 있다. #arg는 0부터 시작하는 인자의 인덱스를 나타난다.iban 또는 a0 (p<#arg>로도 사용가능)
result평가 context메소드 호출 결과. unless와 cache evict표현에서만 사용가능하다.#result

@CacheEvict

@CacheEvict@Cacheable과 반대로 cache저장소의 데이터를 제거함으로써 사용하지 않는 데이터를 정리하는데 유용하다. @CacheEvict는 캐시 삭제를 수행할 메서드에 선언한다. @CacheEvict도 여러 개의 캐시를 명시할 수 있으며, key와 condition을 사용할 수 있다. 또한 allEntries 속성은 키 값으로 Cache Entry 하나만 비우는 것이 아니라 캐시영역의 모든 Entries를 비우도록 한다. 이 경우에는 키를 명시하더라도 이를 무시하고 모든 Entries를 비우게 된다.

@CacheEvict(value = "books", allEntries=true)
public void loadBooks(InputStream batch)

@CachePut

메소드의 흐름을 방해하지 않고 Cache에 저장하거나 업데이트를 해야 하는 경우, @CachePut을 사용한다. 즉, 메소드는 항상 실행되고 그 결과가 캐시에 저장된다. @CachePut@Cacheable과 동일한 옵션을 제공하며, Cache에 저장하는 것보다는 메소드의 흐름을 최적화하는데 사용되어야 한다.

@Cacheable과 함께 사용하는 것은 일반적으로 권장하지 않는다.

@Caching

다수의 Cache annotation을 쓰고자 할 때 @Caching을 쓴다. @Cacheable, @CacheEvic, @CachePut을 지원한다.

@Caching(evict = { @CacheEvict("primary"), @CacheEvict(value = "secondary", key = "#p0") })
public Book importBooks(String deposit, Date date)

XML기반 Caching설정

Java메소드에 Annotation을 붙이는 대신 XML로 caching할 메소드를 정의할 수 있다. 아래에서는 CacheManager bean설정을 생략하였다. BookService 패키지의 하위 메소드 중 findBook 메소드에 cacheable이 적용되며 loadBooks 메소드에 cacheEvict가 적용되었다.

<!-- the service we want to make cacheable -->
<bean id="bookService" class="x.y.service.DefaultBookService"/>
 
<!-- cache definitions -->
<cache:advice id="cacheAdvice" cache-manager="cacheManager">
    <cache:caching cache="books">
        <cache:cacheable method="findBook" key="#isbn"/>
        <cache:cache-evict method="loadBooks" all-entries="true"/>
    </cache:caching>
</cache:advice>
 
<!-- apply the cacheable behavior to all BookService interfaces -->
<aop:config>
    <aop:advisor advice-ref="cacheAdvice" pointcut="execution(* x.y.BookService.*(..))"/>
</aop:config>
...
// CacheManager설정 생략

참고자료

2.22 - Marshalling/Unmarshallig 서비스

Marshalling/Unmarshalling 서비스는 Java 객체를 XML로 변환(Marshalling)하거나, XML을 Java 객체로 변환(Unmarshalling)하는 기술이다. Spring은 이를 위해 다양한 OXM 툴(JAXB, Castor, XMLBeans 등)을 지원하며, 이 기능을 활용해 객체와 XML 간의 데이터 매핑을 쉽게 처리할 수 있다. Castor와 XMLBeans를 이용한 샘플 코드는 각각 XML 문서로 데이터를 저장하고 다시 객체로 변환하는 과정으로 구성되어 있다.

Marshalling/Unmarshallig 서비스

개요

Object/XML Mapping, 줄여서 O/X mapping은 Object를 XML문서로 변환하는데 이를 XML Mashalling 또는 Marshalling 이다. 반대로 XML문서를 Object로 변환하는 것은 Unmarshalling 이다.

설명

Spring Web Service OXM

Client <------ XML ------> Server

WS는 Server와 Client 두 대상 간의 데이터를 주고받는 기술 중 하나이다. 정보를 요청하는쪽이 Client이다.(Client는 Server가 될 수도 있고 일반 사용자가 될 수도 있다.) 요청한 정보를 받아서 알맞게 처리 후 결과값을 리턴하는 쪽이 Server이다.

Client(OXM) <------ XML(WSDL) ------> (OXM)Server

WS는 XML(WSDL)형식으로 데이터를 주고받는다. 따라서 이 XML를 객체화 하거나 객체를 XML화 해야 한다. 그것이 Marshalling,Unmarshalling이다. OXM Utill은 JAXB,Castor,XMLBeans,JiBX,XStream..등 여러 가지가 있다.

Marshalling

Spring의 모든 marshalling 추상 클래스들은 org.springframework.oxm.Marshaller interface를 implemention 한다.

public interface Marshaller {
  /**
   * Marshals the object graph with the given root into the provided Result.
   */
  void marshal(Object graph, Result result)
      throws XmlMappingException, IOException;
}
PARAMETER설 명
ObjectXML문서구조와 같은 Java 객체
javax.xml.transform.ResultXML 출력과 관련된 객체들이 반드시 구현해야 할 Interface
  • java.xml.transform.Result는 XML 출력과 관련된 객체들이 반드시 구현해야 할 Interface다.
  • 구현 객체와 XML표현은 다음과 같이 매칭된다.
javax.xml.transform.ResultXML표현
javax.xml.transform.dom.DOMResultorg.w3c.dom.Node
javax.xml.transform.sax.SAXResultorg.xml.sax.ContentHandler
javax.xml.transform.stream.StreamResultjava.io.OutputStream java.io.File 또는 java.io.Writer

Unmarshalling

Spring의 모든 Unmarshalling 추상 클래스들은 org.springframework.oxm.Unmarshaller interface를 implemention 한다.

public interface Unmarshaller {
  /**
   * Unmarshals the given provided Source into an object graph.
   */
  Object unmarshal(Source source)
      throws XmlMappingException, IOException;
}
PARAMETER설 명
javax.xml.transform.SourceXML source or transformation instructions 등을 매개변수로 받는 StreamSource 객체
  • java.xml.transform.Source는 XML 입력과 관련된 객체들이 반드시 구현해야 할 Interface다.
  • 구현 객체와 XML표현은 다음과 같이 매칭된다.
javax.xml.transform.SourceXML표현
javax.xml.transform.dom.DOMSourceorg.w3c.dom.Node
javax.xml.transform.sax.SAXSourceorg.xml.sax.InputSource org.xml.sax.XMLReader
javax.xml.transform.stream.StreamSourcejava.io.File java.io.InputStream 또는 java.io.Reader

Marshaller 와 Unmarshaller 사용하기

Spring’s OXM은 다양한 Java-XML Binding 오픈소스를 지원한다. 여기서는 오픈소스 CastorXMLBeans를 사용하여 구현한 가이드프로그램을 제시한다.

Castor

Castor XML mapping은 XML Binding 오픈소스 프레임워크이다. Castor는 java object에서 XML문서, XML문서에서 java object로 변환을 지원한다. mapping file을 사용하여 좀더 수월하게 Castor를 사용할 수 있지만 그 외 추가적인 구성은 할 필요가 없다. 좀 더 많은 OpenSource Castor 정보를 원한다면 Castor web siteorg.springframework.oxm.castor package를 참조하면 된다.

Create binding classes
  • XML Schemas를 작성한다.(text.xsd)
  • 다음을 실행하면 스키마에 따라 -package에 지정된 패키지로 클래스들이 생성된다.
  • java org.exolab.castor.builder.SourceGenerator -i text.xsd -package gen.xyz
ARGS설 명
text.xsd참조할 xml 스키마
gen.xyzxml 스키마를 사용하여 Generation한 Java class package
Configuration
<bean id="divertcastor" class="egovframework.rte.fdl.divert.DivertCastor">
        <property name="marshaller" ref="castorMarshaller" />
        <property name="unmarshaller" ref="castorMarshaller" />
</bean>
<bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller"/>
PROPERTIES설 명
marshallerorg.springframework.oxm.castor.CastorMarshaller
unmarshallerorg.springframework.oxm.castor.CastorMarshaller
Sample Source

Java Object의 데이타를 XML문서로 DataBinding Sample Source

@Resource(name = "castorMarshaller")
private Marshaller marshaller;
 
public void testMarshalling() 
{
 try {
       FileOutputStream os = null;
       List<Writer> book2Writers = new ArrayList<Writer>();
       book2Writers.add(new Writer("J,J.R 툴킨"));
       book2Writers.add(new Writer("J.J.T 툴킨"));
       BookMg bookMg2 = new BookMg("9780446618502", "반지의 제왕", book2Writers);
 
       os = new FileOutputStream("CasterBook.xml");
       marshaller.marshal(bookMg2, new StreamResult(os));
 
     } catch (Exception e) 
     {
	logger.debug(e.getMessage());
	e.printStackTrace(System.err);
	fail("testMarshalling failed!");
     }
}
  1. writer를 작가명을 매개변수로 하여 생성한다. (아래 Writer.java 코드 참조)
  2. bookMg를 ISDN,책명,writer를 매개변수로 하여 생성한다. (아래 BookMg.java 코드 참조)
  3. FileOutputSream을 Caster_Book.xml를 매개변수로 하여 Stream를 생성한다.
  4. Result을 생성한 FileOutputStream을 매개변수로 하여 StreamResult를 생성한다.
  5. marshaller.marshal를 사용하여 marshalling을 하는데 매개변수로는 bookMg와 StreamResult로 한다.
  6. 실행후 JavaObject bookMg에 저장한 값이 CasterBook.xml로 Binding 된것을 확인한다. (아래 CasterBook.xml 코드 참조)

Writer.java

public class Writer 
{
  private String Name;
  // 생성자
  public Writer() { }
  // 작가명
  public Writer(String Name) {
    this.Name = Name;
  }
  // 작가명 수정
  public void setName(String Name) {
    this.Name = Name;
  }
  // 작가명 리턴 
   public String getName() {
    return Name;
  }
}

BookMg.java

public class BookMg 
{
  private String isbn;
  private String title;
  private List<Writer> writers;
 
  public BookMg() { }
  // 생성자
  @ isbn 책번호
  @ title 책제목
  @ writers 작가리스트
  public BookMg(String isbn, String title, List<Writer> writers) {
    this.isbn = isbn;
    this.title = title;
    this.writers = writers;
  }
   // 생성자
  @ isbn 책번호
  @ title 책제목
  @ writer 작가
  public BookMg(String isbn, String title, Writer writer) {
    this.isbn = isbn;
    this.title = title;
    this.writers = new LinkedList<Writer>();
    writers.add(writer);
  }
 // 책번호 설정
 @ isbn 책번호
  public void setIsbn(String isbn) {
    this.isbn = isbn;
  }
 // 책번호 리턴 
  public String getIsbn() {
    return isbn;
  }
 // 책제목 설정
 @ title 책제목
  public void setTitle(String title) {
    this.title = title;
  }
 // 책제목 리턴
  public String getTitle() {
    return title;
  }
 // 작가리스트 설정
 @ writers 작가리스트
  public void setWriters(List<Writer> writers) {
    this.writers = writers;
  }
 // 작가리스트 리턴 
  public List<Writer> getWriters() {
    return writers;
  }
 // 작가리스트에 작가 추가
 @ writer 작가
  public void addWriter(Writer writer) {
    writers.add(writer);
  }
}

CasterBook.xml

<?xml version="1.0" encoding="UTF-8"?>
<book-mg>
  <isbn>9780446618502</isbn>
  <writers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xsi:type="java:egovframework.rte.fdl.divert.Writer">
    <name>J,J.R 툴킨</name>
  </writers>
  <writers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xsi:type="java:egovframework.rte.fdl.divert.Writer">
    <name>J.J.T 툴킨</name>
  </writers>
  <title>반지의 제왕2</title>
</book-mg>

**##### XML문서를 JavaObject로 DataBinding Sample Source

@Resource(name = "castorMarshaller")
private Unmarshaller unmarshaller;
 
@Test
public void testUnmarshalling() 
{
   FileInputStream is = null;
  try 
  {
    is = new FileInputStream("CasterBook.xml");
    bookMg = (BookMg) unmarshaller.unmarshal(new StreamSource(is));
    writers = bookMg.getWriters();
    for (Iterator i = writers.iterator(); i.hasNext(); ) 
    {
      Writer writer = (Writer)i.next();
    }
  }catch(FileNotFoundException fnde)
  {
	fnde.getStackTrace();
  }
  catch(IOException ioe)
 {
	ioe.getStackTrace();
 }
 finally 
 {
    if (is != null) 
    {
     try
     {
	is.close();
     }catch(IOException ioe)
     {
	ioe.getStackTrace();
     }
   }
 }
}
  1. FileInputSream을 CasterBook.xml를 매개변수로 하여 Stream를 생성한다. (위 CasterBook.xml 코드 참조)
  2. 생성한 FileInputSream을 매개변수로 하여 StreamSource를 생성한다.
  3. marshaller.marshal를 사용하여 unmarshalling을 하는데 매개변수로는 StreamSource로 한다.
  4. unmarshalling한 결과 bookMg를 얻어 작가 리스트를 얻는다.

XMLBeans

XMLBeans는 스키마 기반으로 XML 인포셋 전체에 커서 기반으로 접근할 수 있도록 하는 XML-Java binding tool이다. BEA Systems에 의해 개발되었으며 2003년에 아파치 프로젝트에 기증 되었다. 좀 더 많은 정보는 XMLBeans web siteorg.springframework.oxm.xmlbeans package를 참조 하면 된다.

Create binding jar
  • XML Schemas를 작성한다.(text.xsd)
  • 다음을 실행하면 스키마에 따라 jar 파일이 생성된다.
  • scomp -out text.jar text.xsd
ARGS설 명
text.jarbinding classes jar
text.xsd참조할 xml 스키마
Configuration
<bean id="divertxmlbeans" class="egovframework.rte.fdl.divert.DivertXMLBeans">
          <property name="marshaller" ref="xmlBeansMarshaller" />
          <property name="unmarshaller" ref="xmlBeansMarshaller" />
</bean>
<bean id="xmlBeansMarshaller" class="org.springframework.oxm.xmlbeans.XmlBeansMarshaller" />
PROPERTIES설 명
marshallerorg.springframework.oxm.xmlbeans.XmlBeansMarshaller
unmarshallerorg.springframework.oxm.xmlbeans.XmlBeansMarshaller
Sample Source

Java Object의 데이터를 XML문서로 DataBinding Sample Source

@Resource(name = "xmlBeansMarshaller")
private Marshaller marshaller;
 
@Test
public void testMarshalling() 
{
  FileOutputStream os = null; 
  userDoc = UserinfoDocument.Factory.newInstance();
  userElement = userDoc.addNewUserinfo();
 
  userElement.setName("홍길동");
  userElement.setAge(31);
  userElement.setPhone(022770918);
 
  xmlOptions = new XmlOptions();
  xmlOptions.setSavePrettyPrint();
  xmlOptions.setSavePrettyPrintIndent(4);
  xmlOptions.setCharacterEncoding("euc-kr");
 
 try 
 {
    os = new FileOutputStream("XMLBeanGen.xml");
    marshaller.marshal(userDoc, new StreamResult(os));
 }catch(Exception ee)
 {
   ee.getStackTrace();
   fail("testMarshalling failed!");
 }
 finally 
 {
    if (os != null) 
    {
      try
      {
	 os.close();
       }catch(IOException e3)
       {
  	 e3.getStackTrace();
	 fail("testMarshalling failed!");
       }
    }
  }
}
  1. UserDoc 인스턴스를 생성한다.
  2. 사용자정보를 저장할 userElement를 추가한다.
  3. setName을 사용하여 사용자명을 저장한다.
  4. setAge를 사용하여 나이를 저장한다.
  5. setPhone를 사용하여 전화번호를 저장한다.
  6. FileOutputSream을 XMLBeanGen.xml을 매개변수로 하여 생성한다. (아래 XMLBeanGen.xml 코드 참조)
  7. marshaller.marshal를 Document객체와 StreamResult를 매개변수로 하여한다.

XMLBeanGen.xml

<?xml version="1.0" encoding="UTF-8"?>
<userinfo>
  <name>홍길동</name>
  <age>31</age>
  <phone>022770918</phone>
</userinfo>

XML문서를 JavaObject로 DataBinding Sample Source

@Resource(name = "xmlBeansMarshaller")
private Unmarshaller unmarshaller;
 
@Test
public void testUnmarshalling() 
{
    FileInputStream is = null;
    try 
    {
	is = new FileInputStream("XMLBeanGen.xml");
	userDoc = (UserinfoDocument) unmarshaller.unmarshal(new StreamSource(is));
	userElement =  userDoc.getUserinfo();
     }catch(FileNotFoundException fnde)
    {
	fnde.getStackTrace();
	fail("testUnmarshalling failed!");
    }
    catch(IOException ioe)
    {
	ioe.getStackTrace();
    }
    finally 
    {
      if (is != null)
      {
	try
	{
	   is.close();
	}catch(IOException ioe)
	{
	  ioe.getStackTrace();
	  fail("testUnmarshalling failed!");
	}
      }
    }
}
  1. FileInputStream를 XMLBeanGen.xml을 매개변수로 하여 생성한다. (위 XMLBeanGen.xml 코드 참조)
  2. 생성한 FileInputStream를 매개변수로 하여 StreamSource생성한다.
  3. unmarshaller.unmarshal를 생성한 StreamSource를 매개변수로 unmashalling을 하여 document객체를 얻는다.
  4. document객체에서 사용자정보를 얻는다.

참고자료

예제

2.23 - XML Manipulation Service

XML Manipulation 서비스는 XML 문서의 생성, 읽기, 쓰기, 수정 등을 위한 기능을 제공하며, DOM과 SAX 두 가지 파서 방식을 지원한다. DOM은 트리 구조로 XML 문서를 다루고, SAX는 이벤트 기반으로 처리한다. 이 서비스를 통해 XML 문서의 요소 추가, 삭제, 수정, XPath 검색 및 Validation 검사 등의 작업을 수행할 수 있다.

XML Manipulation Service

개요

XML Manipulation 서비스는 XML을 생성하고, 읽고, 쓰는 등과 같은 기능과 조작 기능을 제공하는 서비스이다. XML(Extensible Markup Language)은 W3C에서 다른 특수 목적의 마크업 언어를 만드는 용도에서 권장되는 다목적 마크업 언어이다.XML은 SGML의 단순화된 부분집합이지만, 수많은 종류의 데이터를 기술하는데 적용할 수 있다.XML은 주로 다른 시스템, 특히 인터넷에 연결된 시스템끼리 데이터를 쉽게 주고받을 수 있게 하여 HTML의 한계를 극복할 목적으로 만들어졌다. XML은 W3C에서 다른 특수 목적의 마크업 언어를 만드는 용도에서 권장되는 다목적 마크업 언어이다. XML은 SGML의 단순화된 부분집합이지만, 수많은 종류의 데이터를 기술하는데 적용할 수 있다.

설명

XML Parser

XML 문서를 읽어 들이는 역할을 수행하는 파서는 두 가지 종류가 있다. XML 파일의 내용을 트리 구조로 한번에 읽어 들여 객체를 생성하여 처리하는 DOM(Document Object Model) 과 각각의 태그와 내용 등이 인식될 때마다 XML 문서를 읽어 들이는 SAX(Simple API for XML)라는 기술이다.

DOM(Document Object Model)

요약

XML 문서는 요소(element),속성(attribute),Text 등으로 구성된 트리 구조의 계층적인 정보이다. ⇒DOM을 이용하면 XML 문서의 각 요소들에 대하여 트리 구조의 객체를 읽어 들인다. DOM은 XML 문서를 나타내는 각각의 객체들에 대한 표준 인터페이스이다. DOM 파서는 XML 문서로부터 DOM 구조를 생성하는 역할을 한다.

xml-manipulation-service-dom

Sample Source
1) XML 문서에 대한 Document 객체 생성
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder parser = facory.newDocumentBuilder();
Document dc = parser.parse("XML파일명");
Element root = xmldoc.getDocumentElemnt();
logger.debug(root);
  • DocumentBuilderFactory 인스턴스를 생성
  • 생성한 DocumentBuilderFactory를 통해서 parse를 생성
  • DocumentBuilder의 parse() 메서드를 호출하여 XML 문서를 로드
  • 문서의 루트 요소 획득
2) 바로 밑의 자식 요소들 추출 : Node 인터페이스에서 상속한 getFirstChild(),getNextSibling()을 자식 요소들 중 첫 번째 요소와 이 요소의 형제 요소들을 추출한다.
Element root = doc.getDocumentElement();
for (Node ch = root.getFirstChild(); ch != null; ch = ch.getNextSibling() {
    logger.debug(ch.getNodeName());
}
  • 문서의 루트 요소 획득
  • getFirstChild(),getNextSibling()을 자식 요소들 중 첫 번째 요소와 이 요소의 형제 요소들을 추출
3) 요소명만 추출 : 공백을 나타내는 텍스트 노드는 제외하고 요소명만 출력하려면 추출된 노드의 타입을 점검하여 Node.ELEMENT_NODE인 경우에만 노드 명을 출력한다.
Element root = doc.getDocumentElement();
for (Node ch = root.getFirstChild(); ch != null; ch = ch.getNextSibiling()) {
  if (ch.getNodeType() == Node.ELEMENT_NODE)
    logger.debug(ch.getNodeName());
}
  • 문서의 루트 요소 획득
  • 노드의 타입을 점검하여 Node.ELEMENT_NODE인 경우에만 노드 명을 출력
4) 모든 자식 요소 추출 : 각각의 자식 노드에 대해서도 getNode()메서드를 호출하여 자식의 자식 노드들도 추출한다.재귀함수 기법을 활용한다.
Element root = xmldoc.getDocumentElement();
getNode(root)

public static void getNode(Node n) {
  for (Node ch = n.getFirstChild(); ch != null; ch = ch.getNextSibling()) {
    if (ch.getNodeType() == Node.ELEMENT_NODE) {
      logger.debug(ch.getNodeName());
      getNode(ch);
    }
  }
}
  • 노드의 자식(child)노드를 찾아간다
  • getNode()메서드를 호출하여 자식의 자식 노드들도 추출
5) 공백 이외의 텍스트만 추출 : 공백을 나타내는 텍스트 노드를 제외하고 요소의 내용에 대한 텍스트 노드를 추출하려면 다음과 같은 조건식으로 제어문을 사용한다.
public static void getNode(Node n) {
  for (Node ch = n.getFirstChild(); ch != null; ch = ch.getNextSibling()) {
    if (ch.getNodeType() == Node.ELEMENT_NODE) {
      logger.debug(ch.getNodeName());
      getNode(ch);
    }
    // 텍스트를 처리한다.
    else if (ch.getNodeType() == Node.TEXT_NODE && ch.getNodeValue().trim().length() != 0) {
      logger.debug(ch.getNodeValue());
    }
  }
}
  • Ordered List Item
6-1) 노드들의 타입에 따른 Parsing : 각 노드 타입에 따라 처리하고 추출된 XML 문서의 내용을 하나의 문자열로 리턴하는 메서드이다.
private String xmlString = "";
public String printString(Node node) {
  int type = node.getNodeType();
  switch (type) {
  case Node.DOCUMENT_NODE:
    printString(((Document) node).getDocumentElement());
    break;
  case Node.ELEMENT_NODE:
    xmlString += "<" + node.getNodeName();
    NamedNodeMap attrs = node.getAttributes();
    for (int i = 0; i < attrs.getLength(); i++) {
      Node attr = attrs.item(i);
      xmlString += " " + attr.getNodeName() + "=" + attr.getNodeValue() + "'";
      xmlString += ">";
      NodeList children = node.getChildNodes();
      if (children != null) {
        for (int i = 0; i < children.getLength(); i++) {
          logger.debug(children.item(i));
        }
      }
      break;
    }
  case Node.CDATA_SECTION_NODE:
    xmlString += "<![CDATA[" + node.getNodeValue() + "]]>";
    break;
  case Node.TEXT_NODE:
    xmlString += node.getNodeValue().trim();
    break;
  case Node.PROCESSING_INSTRUCTION_NODE:
    xmlString += "<?" + node.getNodeName() + " " + node.getNodeValue() + "?>";
    break;
  }
  if (type == Node.ELEMENT_NODE) {
    xmlString += "</" + node.getNodeName() + ">";
  }
  return xmlString;
}
6-2) 노드들의 타입에 따른 Parsing : Node 인터페이스에서 지원되는 getNodeType()이라는 메서드를 사용하여 인식된 자식 노드가 어떠한 타입 노드인지에 따라 처리한다.
NODE TYPE설 명
Node.DOCUMENT_NODEDocumentElement 객체 정보를 가지고 printString()을 호출
Node.ELEMENT_NODE요소명,속성정보(NAME,VALUE)을 추출하여 xmlString에 저장하고 자손 요소 정보를 가지고 pringString()을 호출
Node.CDATA_SECTION_NODE추출된 VALUE에 <!CDATA[와]]>을 추가
Node.TEXT_NODEVALUE만 추출
Node.PROCESSING_INSTRUCTION_NODE추출된 값에 <? 와 ?>을 추가

SAX(Simple API for XML)

요약

XML 문서를 읽어 들이는 응용 프로그램 API 로서 XML 문서를 하나의 긴 문자열로 간주한다. SAX는 문자열을 앞에서 부터 차례로 읽어 가면서 요소,속성이 인식될 때 마다 EVENT를 발생시킨다.

각각의 EVENT가 발생 될 떄 마다 수행하고자 하는 기능을 이벤트 핸들러 기술을 이용하여 구현한다.

SAX 프로그램구현 과정

xml-manipulation-sax-program

  1. 발생될 이벤트에 대한 핸들러(이벤트 처리 객체)를 개발한다.
  2. SAX 객체를 생성한다.
  3. 미리 구현한 핸들러를 등록하면서 XML 문서를 읽어 들인다.
Sample Source

xml-manipulation-sax-sample

  • 어떠한 기능의 핸들러를 구현할 것인가에 따라 다음 인터페이스들을 필요에 따라 상속한다.
org.xml.sax.ContentHandlder
org.xml.sax.DTDHandler
org.xml.sax.EntityResolver
org.xml.sax.ErrorHandler
  • org.xml.sax.ContentHandler : 이 핸들러는 SAX의 핵심으로,일반적인 문서 이벤트를 처리한다.
void characters(char[] ch,int start,int length)
void endDocument() // 문서의 끝이 인식되면 호출된다.
void endElement(String namespaceURI,String localName,String qName)
                   // 요소의 종료가 인식되면 호출된다.
void endPrefixMapping(String prefix) // prefix-URI 이름 공간의 종료가 인식되면 호출된다.
void ignorableWhitspace(char[] ch,int start, int length)
                   // 요소 내용에서 무시 가능한 공백을 인식되면 호출된다.
void processingInstruction(String target,String data) // PI가 인식되면 호출된다.
void setDocumentLocator(Locator locator) // 이벤트가 발생된 위치 정보를 알려 주는 객체 전달 시 호출된다.
void skippedEntity(Strig name) // 스킵된 엔티티가 인식되면 호출된다.
void startDocument() // 문서의 시작이 인식되면 호출된다.
void startElement(String namespaceURI,String localName,String qName,Attributes atts)
                   // 요소의 시작이 인식되면 호출된다.
void startPrefixMapping(String prefix,String uri) 
                   // "prefix-URI 이름 공간의 시작이 인식되면 호출된다." 가 인식되면 호출된다.
  • org.xml.sax.DTDHandler : 기본적인 파싱에 있어 요구되는 DTD 이벤트를 핸들링하기 위해 호출된다. 즉 표기법과 파싱되지 않는 엔티티(entity)선언을 만날 때 호출 된다.
void notationDecl(String name,String publicId,String systemId) // DTD 선언 이벤트가 인식되면 호출된다.
void unparsedEntityDecl(String name,String publicId,String systemId,String notationName)
                  // 파싱되지 않는 엔티티 선언 이벤트가 인식되면 호출된다.
  • org.xml.sax.EntityResolver : 외부 엔티티를 참조하기 위하여 호출된다. 만일 문서에 외부 엔티티 참조가 없다면, 이 인터페이스는 필요 없다.
InputSource resolveEntity(String publicId,String systemId) // 확장 엔티티 처리와 관련된 기능을 처리한다.
  • org.xml.sax.ErrorHandler : 에러를 처리하기 위해 사용된다. 파서는 모든 경고와 에러를 처리하기 위해 이 핸들러가 호출된다.
void error(SAXParseException exception) // 복구 가능한 오류 발생시 호출된다.
void fatalError(SAXParseException exception) // 복구 불가능한 오류 발생시 호출된다.
void warning(SAXParseException exception) // 경고 오류 발생시 오출된다.
  • DefaultHandler 클래스 : 필요한 메서드만을 오버라이딩 하여 구현할 수 있도록 이 인터페이스를 모두 상속하여 각 인터페이스에 정의되어 있는 abstract메서들을 오버라이딩한 DefaulatHandler 클래스를 사용한다.

xml-manipulation-sax-defaulthandler

//DefaultHandler를 상속하여 구현한 핸들러 클래스
class SampleHandler extends DefaultHandler {
  public void startDocument() {
    logger.debug("XML이 시작되었습니다.");
  }
  public void endDocument() {
    logger.debug("XML이 종료되었습니다.");
  }
}
  • SAX 객체 생성 : DOM 객체 생성 방법과 비슷하게 SAX 객체도 SAXParseFactory 객체 생성 후에 SAXParser 객체를 생성한다.
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser sp = spf.newSAXParser();
  • 핸들러 등록과 XML 문서 읽기 : 핸들러 클래스의 객체와 SAXParser의 객체를 생성한 후에 다음과 같이 여러 개로 오버로딩 된 메서드를 중에서 하나를 선택하여 XML을 SAX 객체로 읽어 들인다.
void parse(File f,DefaultHandler dh)
void parse(InputStream is, DefaultHandler dh)
void parse(InputStream is, DefaultHandler dh)
void parse(String uri, DefaultHandler dh)
// 핸들러를 등록하면서 XML 문서를 읽는 예
SampleHandler sh = new SampleHandler();
// 문서를 읽어들린다.
sp.parse(new FileInputStream("text.xml"),sh);

XML Manipulation

Configuration

<bean id="xmlCofig" class="egovframework.rte.fdl.xml.EgovXmlset">
        <property name="domconcrete" ref="domconcreteCont" />
        <property name="saxconcrete" ref="saxconcreteCont" />
</bean>
<bean id="domconcreteCont" class="egovframework.rte.fdl.xml.EgovConcreteDOMFactory"/>
<bean id="saxconcreteCont" class="egovframework.rte.fdl.xml.EgovConcreteSAXFactory"/>
PROPERTIES설 명
domconcreteEgovDOMValidatorService 생성하는 Concrete Class
saxconcreteEgovSAXValidatorService 생성하는 Concrete Class
<context:property-placeholder location="classpath*:spring/egovxml.properties" />
<bean id="xmlconfig" class="egovframework.rte.fdl.xml.XmlConfig">
	 <property name="xmlpath" value="${egovxmlsaved.path}" />
</bean>
PROPERTIES설 명
xmlpathXML문서 생성 Directory 위치 지정
// XML 기본저장 디렉토리
egovxmlsaved.path=C:\\Temp\\

egovxml.properties 내용 예시

Sample Source

/** abstractXMLFactoryService 상속한 Class **/
@Resource(name = "domconcreteCont")
EgovConcreteDOMFactory domconcrete = null;
 
/** abstractXMLFactoryService 상속한 Class **/
@Resource(name = "saxconcreteCont")
EgovConcreteSAXFactory saxconcrete = null;
 
/** AbstractXMLUtility 상속한 DOMValidator **/
EgovDOMValidatorService domValidator = null;
/** AbstractXMLUtility 상속한 SAXValidator **/
EgovSAXValidatorService saxValidator = null;
DOM Service 생성
@Test
public void ModuleTest() throws UnsupportedException {
  domValidator = domconcrete.CreateDOMValidator();
  logger.debug("fileName :" + fileName);
  domValidator.setXMLFile(fileName);
}
SAX Service 생성
@Test
public void ModuleTest() throws UnsupportedException {
  saxValidator = saxconcrete.CreateSAXValidator();
  logger.debug("fileName :" + fileName);
  saxValidator.setXMLFile(fileName);
}
well-formed, Validation 검사

XML문서의 well-formed 검사를 하면서 Validation검사도 동시에 실행(선택)

public void WellformedValidate(boolean used, boolean isvalid, AbstractXMLUtility service) throws ValidatorException {
  if (used) {
    if (service.parse(isvalid)) {
      if (isvalid)
        logger.debug("Validation 문서입니다.");
      else
        logger.debug("well-formed 문서입니다.");
    }
  }
}
PARAMETER설 명
isvalidValidation 검사여부
XPATH 조회

검색하고자하는 Element나 Attribute등을 검색식(표현식)을 통해 조회

public void XPathResult(boolean used, AbstractXMLUtility service, Document doc) throws JDOMException {
  if (used) {
    List list = service.getResult(doc, "//*[@*]");
    viewEelement(list);
  }
}
PARAMETER설 명
exprValidation 검색식
docDocument 객체
XML 생성

입력받은 Element를 사용하여 XML문서를 생성

public void createNewXML(boolean used, AbstractXMLUtility service, Document doc, String EleName, List list, String path)
throws JDOMException, TransformerException, FileNotFoundException {
  if (used)
    service.createNewXML(doc, EleName, list, path);
}
PARAMETER설 명
docDocument 객체
EleNameRoot 명
list생성 Element List
path생성될 XML문서 경로
Element 추가

입력받은 Element를 XML문서에 추가

public void addElement(boolean used, AbstractXMLUtility service, Document doc, String EleName, List list, String path)
throws JDOMException, TransformerException, FileNotFoundException {
  if (used)
    service.addElement(doc, EleName, list, path);
}
PARAMETER설 명
docDocument 객체
EleNameRoot 명
list생성 Element List
path생성될 XML문서 경로
TextNode Element 추가

입력받은 Text Element를 XML문서에 추가

public void addTextElement(boolean used, AbstractXMLUtility service, Document doc,
  String elemName, List list, String path)
throws JDOMException, TransformerException, FileNotFoundException {
  if (used)
    service.addTextElement(doc, elemName, list, path);
}
PARAMETER설 명
docDocument 객체
EleNameRoot 명
list생성 Element List
path생성될 XML문서 경로
TextNode Element 수정

입력받은 Text Element로 수정

public void updTextElement(boolean used, AbstractXMLUtility service, Document doc, List list, String path)
throws JDOMException, TransformerException, FileNotFoundException {
  if (used)
    service.updTextElement(doc, list, path);
}
PARAMETER설 명
docDocument 객체
list생성 Element List
path생성될 XML문서 경로
Element 삭제

입력받은 Element을 삭제

public void delElement(boolean used, AbstractXMLUtility service, Document doc, String EleName, String path)
throws JDOMException, TransformerException, FileNotFoundException {
  if (used)
    service.delElement(doc, EleName, path);
}
PARAMETER설 명
docDocument 객체
EleNameElement 명
path생성될 XML문서 경로
Element 수정
public void updElement(boolean used, AbstractXMLUtility service, Document doc,
  String oldElement, String newElement, String path)
throws JDOMException, TransformerException, FileNotFoundException {
  if (used)
    service.updElement(doc, oldElement, newElement, path);
}
PARAMETER설 명
docDocument 객체
oldElement수정할 Element 명
newElement수정 Element 명
path생성될 XML문서 경로

가이드프로그램 실행순서

  1. 실행관리 Repository에서 egovframework.rte.fdl.xml-1.0.0-SNAPSHOT.jar를 Classpath 설정한다.
  2. spring폴더를 생성 후 Classpath 설정한다.
  3. 실행관리 Repository에서 egovxmlCfg.xml와 egovxml.properties 2파일은 반드시 spring폴더로 복사한다.
  4. egovxmlCfg.xml 내용은 egovxml.properties 파일의 위치를 바꿔 설정한다.
  5. egovxml.properties의 egovxmlsaved.path를 바꿔 설정한다.
  6. 테스트에 필요한 context-test.xml,context-xmltest.xml를 spring폴더 또는 설정한위치로 복사한다.
  7. 설정이 완료된 후 ControlXMLTest.java를 실행한다.

2.24 - Object Pooling Service

Object Pooling 서비스는 객체를 미리 생성해 Pool에서 재사용하여 성능을 향상시키는 방식이다. ObjectPool 인터페이스는 객체 할당과 반환을 처리하며, PooledObjectFactory는 객체의 생성, 유효성 검사, 재초기화 등의 생애주기 관리를 담당한다. BaseObjectPoolBasePooledObjectFactory는 이들을 추상적으로 구현한 클래스들로, 다양한 객체 풀링을 지원한다.

Object Pooling Service

개요

객체에 대한 Pooling 기능을 제공하는 서비스이다. 객체의 생성 비용이 크고,생성 횟수가 많으면, 평균적으로 사용되는 객체의 수가 적은 경우,성능을 향상시키기 위해서 사용한다. Object Pool은 소프트웨어 디자인 패턴으로서, 객체를 필요에 따라 생성하고 파괴하는 방식이 아닌,적절한 개수의 객체를 미리 사용 가능한 상태로 생성하여 이를 이용하는 방식이다.Client는 Pool에 객체를 요청하여 객체를 얻은 후, 업무를 수행한다. 얻어온 객체를 이용하여 업무 수행을 끝마친 후, 객체를 파괴하는 것이 아니라 Pool에게 돌려주어 다른 Client가 사용할 수 있도록 한다. Object Pooling은 객체 생성 비용이 크고,객체 생성 횟수가 많으며,평균적으로 사용되는 객체의 수가 적은 경우,높은 성능의 향상을 가져다 준다.

설명

ObjectPool

ObjectPool은 아래와 같은 interface이다.

public interface ObjectPool {
    Object borrowObject();
    void returnObject(Object borrowed);
}

ObjectPool interface를 구현하여 코드를 작성하여 사용한다.

BaseObjectPool

ObjectPool을 구현한 추상 클래스이다.

public abstract class BaseObjectPool<T> implements ObjectPool<T> {
 
    public abstract T borrowObject() throws Exception;
    public abstract void returnObject(T obj) throws Exception;
    public abstract void invalidateObject(T obj) throws Exception;
 
    public int getNumIdle() throws UnsupportedOperationException {
        return -1;
    }
 
    public int getNumActive() throws UnsupportedOperationException {
        return -1;
    }
 
    public void clear() throws Exception, UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }
 
    public void addObject() throws Exception, UnsupportedOperationException {
        throw new UnsupportedOperationException();
    }
 
    public void close() {
        closed = true;
    }
 
    protected final boolean isClosed() {
        return closed;
    }
 
    protected final void assertOpen() throws IllegalStateException {
        if(isClosed()) {
            throw new IllegalStateException("Pool not open");
        }
    }
 
    private volatile boolean closed = false;
}
METHOD설 명
borrowObject객체 할당
returnObject객체 반환
invalidateObject객체 할당 시 유효성검사
getNumIdleIdle 상태 객체 수 리턴
getNumActiveActive 상태 객체 수 리턴
clear객체 삭제
addObjectPool에 객체 추가
isClosed객체 close여부 판단
assertOpen객체가 사용가능한 상태판단

KeyedObjectPool

KeyedObjectPool은 이기종(異機種) 객체들로 구성된 pool 구현을 위한 interface

public interface KeyedObjectPool {
    Object borrowObject(Object key);
    void returnObject(Object key, Object borrowed);
}
METHOD설 명
borrowObject객체 할당
returnObject객체 반환

PooledObjectFactory

org.apache.commons.pool package는 Object Pool객체 생성과 pool로부터 생성되는 object의 created와 destoryed 부분을 분리하여 구현 할 수 있도록 지원한다. PooledObjectFactory는 pooled object의 생존주기 관리를 위한 interface이다.

public interface PooledObjectFactory<T> {
  PooledObject<T> makeObject() throws Exception;
  void destroyObject(PooledObject<T> p) throws Exception;
  boolean validateObject(PooledObject<T> p);
  void activateObject(PooledObject<T> p) throws Exception;
  void passivateObject(PooledObject<T> p) throws Exception;
METHOD설 명
makeObject객체 생성
activateObjectPool로 부터 객체를 할당 받을 때 호출된다(재초기화 할 때 이용).
passivateObjectPool로 객체를 반환할 때 호출된다(초기화 할 때 이용).
validateObject객체가 유효한지 측정하기 위해 호출된다.
destroyObject객체를 삭제할 때 호출된다.

ObjectPool 구현한 프로그램은 PoolableObjectFactory를 구현한 프로그램을 받아들이도록 구현한다면, 다양하고 독특한 ObjectPool을 구현 할 수 있다.

BasePooledObjectFactory

PooledObjectFactory를 구현한 추상 클래스이다.

public abstract class BasePooledObjectFactory<T> implements PooledObjectFactory<T> {
 
    public abstract T create() throws Exception;
 
    public abstract PooledObject<T> wrap(T obj);
 
    @Override
    public PooledObject<T> makeObject() throws Exception {
        return wrap(create());
    }
 
    @Override
    public void destroyObject(PooledObject<T> p)
        throws Exception  {
    }
 
    @Override
    public boolean validateObject(PooledObject<T> p) {
        return true;
    }
 
    @Override
    public void activateObject(PooledObject<T> p) throws Exception {
    }
 
    @Override
    public void passivateObject(PooledObject<T> p)
        throws Exception {
    }
}

KeyedPooledObjectFactory

KeyedObjectPools를 위한 PooledObjectFactory이다.

public interface KeyedPooledObjectFactory<K,V> {
    PooledObject<V> makeObject(K key) throws Exception;
    void destroyObject(K key, PooledObject<V> p) throws Exception;
    boolean validateObject(K key, PooledObject<V> p);
    void activateObject(K key, PooledObject<V> p) throws Exception;
    void passivateObject(K key, PooledObject<V> p) throws Exception;
}
METHOD설 명
makeObject객체 생성
activateObjectPool로 부터 객체를 할당 받을 때 호출된다(재초기화 할 때 이용).
passivateObjectPool로 객체를 반환할 때 호출된다(초기화 할 때 이용).
validateObject객체가 유효한지 측정하기 위해 호출된다.
destroyObject객체를 삭제할 때 호출된다.

BaseKeyedPoolableObjectFactory

KeyedPoolableObjectFactory를 구현한 추상 클래스이다.

public abstract class BaseKeyedPooledObjectFactory<K,V>
        implements KeyedPooledObjectFactory<K,V> {
 
    public abstract V create(K key)
        throws Exception;
 
    public abstract PooledObject<V> wrap(V value);
 
    @Override
    public PooledObject<V> makeObject(K key) throws Exception {
        return wrap(create(key));
    }
 
    @Override
    public void destroyObject(K key, PooledObject<V> p)
        throws Exception {
    }
 
    @Override
    public boolean validateObject(K key, PooledObject<V> p) {
        return true;
    }
 
    @Override
    public void activateObject(K key, PooledObject<V> p)
        throws Exception {
    }
 
    @Override
    public void passivateObject(K key, PooledObject<V> p)
        throws Exception {
    }
}

2.25 - Encryption/Decryption Service

암호화는 평문을 암호문으로 변환해 메시지를 보호하며, 복호화는 이를 다시 평문으로 복원하는 과정으로, 암호계는 관용암호와 공개키 시스템으로 구분된다.

Encryption/Decryption Service

개요

암호화는 시큐리티에 대처하는 가장 강력한 수단이다. 이때 본래의 메시지를 평문(Plan Text,Clear Text)이라고 부르고, 암호화된 메시지는 암호문(Cipher Text,Cryptogram)이라고 부른다. 암호화(Encryption,Ciphering)는 메시지의 내용이 불명확하도록 평문을 재구성하여 암호문을 만드는 것인데, 이 때 사용되는 메시지의 재구성 방법을 암호화 알고리즘(Encryption Algorithm)이라고 부른다. 암호화 알고리즘에서는 암호화의 비밀성을 높이기 위해 키(Key)를 사용하기도 한다. 복호화(Decyption,decipheing)란 암호화의 역과정으로, 불명확한 메시지로부터 본래의 메시지를 환원하는 과정이다. 일반적으로 복호화에도 암호화에 사용된 것과 동일한 알고리즘이 사용된다. 그리고 암호화 기법을 적용하는 암호화 및 복호화 과정으로 구성된 시스템을 암호계(Crypto System)라고 부른다. 암호계에는 키나 알고리즘이 포함되는데 하나의 비밀키(Private Key,Secret Key)를 암호화와 복호화에 모두 사용하는 관용암호계(Conventional Crypto System) 와 비밀키와 공개키를 사용하는 공개키(Public Key System)시스템으로 구분된다.

설명

JASYPT(Java simplified encryption)

Jasypt는 오픈소스 Java library로 개발자는 암호화관련 깊은 지식이 없어도 암복화 프로그램을 개발할 수 있도록 지원한다. 여기서 설명하는 부분은 암복화 모듈로 사용한 API 중심으로 설명하겠다.

Encryption binaries

Jasypt은 binaries(byte[] objects) 암호화를 위해 org.jasypt.encryption.pbe.PBEByteEncryption interface를 구현한 org.jasypt.encryption.pbe.StandardPBEByteEncryptor를 제공한다.

Configuration

PBE (Password Based Encryption) operations인 org.jasypt.encryption.pbe.StandardPBEByteEncryptor는 암호와 키를 필요하는데 알고리즘인데 설정하는 방법은 아래와 같다

  • package에서 제공하는 기본 설정값 사용(암호는 제외)
  • org.jasypt.encryption.pbe.config.PBEConfig 설정
  • setAlgorithm(…), setProvider(…), setProviderName(…), setPassword(…), setKeyObtentionIterations(…) or setSaltGenerator(…) methods 설정
Initialization

암호화 하기전에 encrptor는 초기화(intialized)되어야 한다. 초기화는 아래와 같다.

  • initialize() 메소드를 호출.
  • encrypt(…) 또는 decrypt(…) 메소드가 처음 호출.(initialize() 메소드가 호출 하지 않아도 초기화됨)
  • encryptor가 초기화가 된 후에 설정이 바뀌어 다시 initialize() 메소드를 호출하면 AlreadyInitializedException 이 발생.
Usage
  • 메시지 암호화할 경우 encrypt(..) 메소드 호출
  • 메시지 복호화할 경우 decrypt(..) 메소드 호출

Encrypting texts

Jasypt은 texts 암호화를 위해 org.jasypt.encryption.pbe.PBEStringEncryptor interface를 구현한 org.jasypt.encryption.pbe.StandardPBEStringEncryptor를 제공한다.

Basics

Jasypt는 text 암호화에 byte(binary) 암호화 방법을 사용한다.

  • 모든 결과(암호화) 문자열은 기본적으로 BASE64로 encode되고 US-ASCII 문자셋으로 안전하게 저장된다.
  • setStringOutputType 메소드를 사용하여 encoding 방식을 선택 할 수 있다.
Configuration

PBE(Password Based Encryption) operations인 org.jasypt.encryption.pbe.StandardPBEStringEncryptor는 암호와 키를 필요하는데 알고리즘인데 설정하는 방법은 아래와 같다

  • package에서 제공하는 기본 설정값 사용(암호는 제외)
  • org.jasypt.encryption.pbe.config.PBEConfig 설정
  • setAlgorithm(…), setProvider(…), setProviderName(…), setPassword(…), setKeyObtentionIterations(…) or setSaltGenerator(…) methods 설정
Initialization

암호화 하기전에 encrptor는 초기화(intialized)되어야 한다. 초기화는 아래와 같다.

  • initialize() 메소드를 호출.
  • encrypt(…) 또는 decrypt(…) 메소드가 처음 호출.(initialize() 메소드가 호출 하지 안아도 초기화됨)
  • encryptor가 초기화가 된 후에 설정이 바뀌어 다시 initialize() 메소드를 호출하면 AlreadyInitializedException 이 발생.
Usage
  • 메시지 암호화할 경우 encrypt(..) 메소드 호출
  • 메시지 복호화할 경우 decrypt(..) 메소드 호출

Encrypting passwords

Jasypt는 외부로 부터 공격받을 수 있는 데이타베이스 암호나 시스템 암호를 암호화할 수 있도록 지원한다.

Jasypt’s Standard[Byte|String]Digester를 사용한 Digest 생성 절차
  1. 명시된 salt size는 생성된다(org.jasypt.salt.SaltGenerator 참고). size가 0일 경우는 salt는 사용되지 않는다. 보다 높은 보안을 위해서는 salt는 기본으로 random으로 생성(like org.jasypt.salt.RandomSaltGenerator)한다.
  2. salt bytes는 메시지 시작부분에 덧붙여진다.
  3. Hash function은 salt와 메시지 전체에 적용되고, function 결과들은 명시한 iterations 만큼 반복된다.
  4. 랜덤으로 salt를 생성하면 undigested salt는 hash 결과 시작부분에 덧붙여진다.

alt text

Encrypting numbers

Jasypt은 numbers 암호화를 위해 org.jasypt.encryption.pbe.PBEBingIntegerEncryptororg.jasypt.encryption.pbe.PBEBingDecimalEncryptor interface를 구현한 org.jasypt.encryption.pbe.StandardPBEBigIntegerEncryptororg.jasypt.encryption.pbe.StandardPBEBigDecimalEncryptor을 제공한다.

Basics

Jasypt는 number 암호화에 byte(binary) 암호화 방식을 사용한다.(Text 암호화 방식과 같다.)

Configuration

PBE (Password Based Encryption) operations인 org.jasypt.encryption.pbe.StandardPBEByteEncryptor는 암호와 키를 필요하는데 알고리즘인데 설정하는 방법은 아래와 같다.

  • package에서 제공하는 기본 설정값 사용(암호는 제외)
  • org.jasypt.encryption.pbe.config.PBEConfig 설정
  • setAlgorithm(…), setProvider(…), setProviderName(…), setPassword(…), setKeyObtentionIterations(…) or setSaltGenerator(…) methods 설정
Initialization

암호화 하기전에 encrptor는 초기화(intialized)되어야 한다. 초기화는 아래와 같다.

  • initialize() 메소드를 호출.
  • encrypt(…) 또는 decrypt(…) 메소드가 처음 호출.(initialize() 메소드가 호출 하지 안아도 초기화됨)
  • encryptor가 초기화가 된 후에 설정이 바뀌어 다시 initialize() 메소드를 호출하면 AlreadyInitializedException 이 발생.
Usage
  • 메시지 암호화할 경우 encrypt(..) 메소드 호출
  • 메시지 복호화할 경우 decrypt(..) 메소드 호출

Using Jasypt with JCE providers

Jasypt는 암호 기반 알고리즘외에 어떠한 알고리즘도 java의 security.provider를 구현한 JCE provider를 사용한다면 사용할 수 있다.

How can you use your own providers in jasypt?
...
Security.addProvider(new BouncyCastleProvider());
...
StandardPBEStringEncryptor mySecondEncryptor = new StandardPBEStringEncryptor();
mySecondEncryptor.setProviderName("BC"); // jce provider name
mySecondEncryptor.setAlgorithm("PBEWITHSHA256AND128BITAES-CBC-BC");
mySecondEncryptor.setPassword(myPassword);
 
String mySecondEncryptedText = mySecondEncryptor.encrypt(myText);
...
...        
StandardStringDigester digester = new StandardStringDigester();
digester.setProvider(new BouncyCastleProvider()); // create jce provider instance 
digester.setAlgorithm("WHIRLPOOL");
 
String digest = digester.digest(message);
...

ARIA 블록암호 알고리즘

ARIA는 경량 환경 및 하드웨어 구현을 위해 최적화된, Involutional SPN 구조를 갖는 범용 블록 암호 알고리즘이다.

  • 블록 크기 : 128비트
  • 키 크기 : 128/192/256비트(AES와 동일 규격)
  • 전체 구조 : Involutional Substitution-Permutation Network
  • 라운드 수 : 12/14/16(키 크기에 따라 결정됨)

ARIA는 경량 환경 및 하드웨어에서의 효율성 향상을 위해 개발되었으며, ARIA가 사용하는 대부분의 연산은 XOR과 같은 단순한 바이트 단위 연산으로 구성되어 있습니다. ARIA라는 이름은 Academy(학계),Research Institute(연구소),Agency(정부 기관)의 첫 글자들을 딴 것으로, ARIA 개발에 참여한 학.연.관의 공동 노력을 표현하고 있다.

표준화 동향

ARIA는 지난 2004년에 국가표준기본법에 의거, 지식경제부에 의하여 국가표준(KS)으로 지정되었다.

  • 표준번호 : KSX1213:2004
  • 부 문 : X-정보산업 < 정보기술(IT)응용
  • 표 준 명 : 128비트 블록 암호 알고리즘 ARIA(128 bit block encryption algorithm ARIA)
  • 이 력 : 2004년 12월 30일 제정
  • 적용범위: 이 규격은 가변 크기의 암호키를 사용하여 128bit 블록 단위로 데이터의 암호화, 복호화를 수행하는 블록 암호 알고리즘을 규정

안전성과 효율성

ARIA는 블록 암호에 대한 알려진 모든 공격에 대한 내성을 갖도록 설계되었다. 일차적으로 설계자들에 의한 내부적인 안전성 분석을 거친 뒤에, 객관적인 안전성 및 효율성 평가를 위하여 NESSIE(New European Schemes for Signatures, Integrity and Encryption)의 주관 기관인 벨기에 루벤 대학으로부터 평가를 받았다. ARIA는 하드웨어 구현 및 8비트 환경에서 뛰어난 효율성을 가지고 있어 IC 카드, VPN 장비 등 다양한 환경에 적용이 가능합니다. 또한 소프트 웨어 구현에서도 벨기에 루벤 대학의 효율성 평가에서 Camellia보다 빠르고 AES에 근접하는 성능을 보였다.

ARIA 효율성 비교(단위: cycle/byte)

CPUARIAAESCamelliaSEED
Pentium III37.323.333.442.4
Pentium IV49.030.583.981.3

이 효율성 비교표는 NESSIE의 효율성 분석 보고서와 루벤 대학의 ARIA 분석 보고서에 근거하여 작성되었다. ARIA는 128비트 키 길이의 경우 루벤 대학의 평가의 대상이었던 ver. 0.8에 비해 라운드 수가 10에서 12로 증가 하였기 때문에 사이클 수를 평가 자료의 120%로 산출하였으며, 두 보고서가 같은 기관에서 (루벤 대학의 COSIC 그룹) 수행되었으나 양쪽 자료가 정확히 같은 환경에서 수행된 것은 아니기 때문에 두 보고서의 AES, Camellia에 대한 데이터로부터 양쪽 플랫폼의 성능비를 산출하여 SEED의 속도를 추정하였다.

가이드프로그램(Guide Program)

Configuration

암복호화 서비스를 사용하기 위해서는 다음과 같이 패스워드에 대한 hash 값 기록이 필요하다.

# Message digest algorithm using EgovPasswordEncoder..
crypto.password.algorithm=SHA-256
 
# hashed password (ex: egovframe (SHA-256) => gdyYs/IZqY86VcWhT8emCYfqY1ahw2vtLG+/FzNqtrQ=)
crypto.hashed.password=gdyYs/IZqY86VcWhT8emCYfqY1ahw2vtLG+/FzNqtrQ=
  • crypto.password.algorithm : 패스워드 인코더에 사용될 hash function 알고리즘 (default : SHA-256)
  • crypto.hashed.password : 패스워드에 대한 hash value (egovframework.rte.fdl.cryptography.EgovPasswordEncoder의 main 메소드에 의해 해당 값을 얻어 기록한다.)

그리고 이를 사용하기 위해 다음과 같이 property-placeholder 설정이 필요하다.

<context:property-placeholder 
location="classpath*:/META-INF/spring/crypto_config.properties,classpath*:/META-INF/spring/password.properties" />
<!-- recommended location method is using file prefix.. ex) "file:/home/properties/crypto_config.properties" -->

※ 위 property 파일과 이를 사용하기 위한 property-placeholder를 사용하지 않고 아래의 xml 설정에 직접 기록하여도 된다.

<bean id="passwordEncoder" class="egovframework.rte.fdl.cryptography.EgovPasswordEncoder">
  <property name="algorithm" value="${crypto.password.algorithm}" /><!-- default : SHA-256 -->
  <property name="hashedPassword" value="${crypto.hashed.password}" />
</bean>
 
<bean id="ARIACryptoService" class="egovframework.rte.fdl.cryptography.impl.EgovARIACryptoServiceImpl">
  <property name="passwordEncoder" ref="passwordEncoder" />
  <property name="blockSize" value="1025" /><!-- default : 1024 -->
</bean>
 
<bean id="digestService" class="egovframework.rte.fdl.cryptography.impl.EgovDigestServiceImpl">
  <property name="algorithm" value="SHA-256" /><!-- default : SHA-256 -->
  <property name="plainDigest" value="false" /><!-- default : false -->
</bean>
 
<bean id="generalCryptoService" class="egovframework.rte.fdl.cryptography.impl.EgovGeneralCryptoServiceImpl">
  <property name="passwordEncoder" ref="passwordEncoder" />
  <property name="algorithm" value="PBEWithSHA1AndDESede" /><!-- default : PBEWithSHA1AndDESede -->
  <property name="blockSize" value="1024" /><!-- default : 1024 -->
</bean>
Example
beanclass설 명
passwordEncoderEgovPasswordEncoderHash function 알고리즘 및 hashed된 패스워드 보관 (다른 암복호화 서비스 bean에 의해 사용됨)
ARIACryptoServiceEgovARIACryptoServiceImplARIA 알고리즘을 통한 암복호화 서비스 제공
digestServiceEgovDigestServiceImplDigest Service(Hash function) 서비스 제공
generalCryptoServiceEgovGeneralCryptoServiceImplARIA 이외의 알고리즘(JASYPT 기반)을 통한 암복호화 서비스 제공

ARIA 알고리즘 Sample Source

Encryption texts Guide Program
@Resource(name="ARIACryptoService")
EgovCryptoService cryptoService;

@Test
public void testString() {
String[] testString = {
    "This is a testing...\nHello!",
    "한글 테스트입니다...",
    "!@#$%^&*()_+|~{}:\"<>?-=\\`[];',./"
};

try {
    for (String str : testString) {
    byte[] encrypted = cryptoService.encrypt(str.getBytes("UTF-8"), password);

    byte[] decrypted = cryptoService.decrypt(encrypted, password);

    assertEquals(str, new String(decrypted, "UTF-8"));
    }
} catch (UnsupportedEncodingException uee) {
    uee.printStackTrace();
    fail();
}
}
Encryption File Guide Program
@Resource(name="ARIACryptoService")
EgovCryptoService cryptoService;
 
@Test
public void testFile() {
    String filePath = "/META-INF/spring/file/test.hwp";
    File srcFile = new File(this.getClass().getResource(filePath).getFile());
 
    File trgtFile;
    File decryptedFile;
    try {
        trgtFile = File.createTempFile("tmp", "encrypted");
        trgtFile.deleteOnExit();
 
        cryptoService.encrypt(srcFile, password, trgtFile);
 
        decryptedFile = File.createTempFile("tmp", "decrypted");
        decryptedFile.deleteOnExit();
 
        cryptoService.decrypt(trgtFile, password, decryptedFile);
 
        assertTrue("Decrypted file not same!!", 
          checkFileWithHashFunction(srcFile, decryptedFile));
    } catch (Exception ex) {
        ex.printStackTrace();
        fail(ex.getMessage());
    }
}

Digest Sample Source

Encryption Digest Guide Program
@Resource(name="digestService")
EgovDigestService digestService;

@Test
public void testDigest() {
    String data = "egovframe";

    byte[] digested = digestService.digest(data.getBytes());

    assertTrue(digestService.matches(data.getBytes(), digested));
}

General 알고리즘 Sample Source

Encryption texts Guide Program
@Resource(name="generalCryptoService")
EgovCryptoService cryptoService;

@Test
public void testString() {
    String[] testString = {
        "This is a testing...\nHello!",
        "한글 테스트입니다...",
        "!@#$%^&*()_+|~{}:\"<>?-=\\`[];',./"
    };

    try {
        for (String str : testString) {
        byte[] encrypted = cryptoService.encrypt(str.getBytes("UTF-8"), password);

        byte[] decrypted = cryptoService.decrypt(encrypted, password);

        assertEquals(str, new String(decrypted, "UTF-8"));
        }
    } catch (UnsupportedEncodingException uee) {
        uee.printStackTrace();
        fail();
    }
}
Encryption BigDecimal Guide Program
@Resource(name="generalCryptoService")
EgovCryptoService cryptoService;

@Test
public void testBigDecimal() {
    BigDecimal big = new BigDecimal(123456);

    BigDecimal encrypted = cryptoService.encrypt(big, password);

    BigDecimal decrypted = cryptoService.decrypt(encrypted, password);

    assertEquals(big, decrypted);
}
Encryption File Guide Program
@Resource(name="generalCryptoService")
EgovCryptoService cryptoService;

@Test
public void testFile() {
    String filePath = "/META-INF/spring/file/test.hwp";

    File srcFile = new File(this.getClass().getResource(filePath).getFile());

    File trgtFile;
    File decryptedFile;
    try {
        trgtFile = File.createTempFile("tmp", "encrypted");
        trgtFile.deleteOnExit();
        //trgtFile = new File("C:/test.enc");

        //System.out.println("Temp file : " + trgtFile.toString());

        cryptoService.encrypt(srcFile, password, trgtFile);

        decryptedFile = File.createTempFile("tmp", "decrypted");
        decryptedFile.deleteOnExit();
        //decryptedFile = new File("C:/test.dec.hwp");

        cryptoService.decrypt(trgtFile, password, decryptedFile);

        assertTrue("Decrypted file not same!!", checkFileWithHashFunction(srcFile, decryptedFile));

    } catch (IOException ioe) {
        ioe.printStackTrace();
        fail(ioe.getMessage());
    } catch (Exception ex) {
        ex.printStackTrace();
        fail(ex.getMessage());
    }
}
Web Encryption Guide Program

ARIA 알고리즘으로 암/복호화시 Byte[] 형태로 결과가 나오기 때문에, Base64를 통해 추가로 인코딩/디코딩 해야 한다. Base64 이용시 Apache Commons Codec의 Base64 클래스를 이용한다.

import org.apache.commons.codec.binary.Base64;

String strValue = "TEXT";

byte[] text = cryptoService.encrypt(strValue.getBytes("UTF-8"), password);
String base64enc = Base64.encodeBase64String(text);
String urlText = URLEncoder.encode(base64enc);

//Decryption
System.out.println("\n\nDecoding Test!!!");

String value = URLDecoder.decode(urlText);
byte[] base64dec = Base64.decodeBase64(value);
byte[] dectext = cryptoService.decrypt(base64dec, password);

pom.xml에 아래 내용을 추가한다.

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.9</version>
</dependency>

참고자료

ARIA 블록암호 알고리즘

2.26 - Crypto 간소화 서비스

표준프레임워크 3.8부터 ARIA 알고리즘을 기반으로 XML Schema와 properties 파일을 통해 간편하게 암/복호화 설정과 중요한 정보 보호를 지원한다.

Crypto 간소화 서비스

개요

표준프레임워크 3.8 부터 ARIA 블록암호 알고리즘 기반 암/복호화 설정을 간소화 할 수 있는 방법을 제공한다. 내부적으로 필요한 설정을 가지고 있고, XML Schema를 통해 필요한 설정만을 추가할 수 있도록 제공한다. 또한 globals.properties 설정 파일의 중요 정보 Url, UserName, Password 항목을 암/복호화 처리 할 수 있도록 제공한다. 그외에 정보는 properties 파일에 암호화 데이터 설정후 #{egovEnvCryptoService.decrypt(’…’)} 복호화 기능을 제공한다.

XML namespace 및 schema 설정

설정 간소화 기능을 사용하기 위해서는 다음과 같은 xml 선언이 필요하다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:egov-crypto="http://maven.egovframe.go.kr/schema/egov-crypto"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://maven.egovframe.go.kr/schema/egov-crypto http://maven.egovframe.go.kr/schema/egov-crypto/egov-crypto-4.2.0.xsd">

Crypto Config 설정

Security에 대한 기본 설정 정보를 제공한다. algorithmKey, algorithmKeyHash 값을 하단 [Crypto algorithmKey, algorithmKeyHash 생성]에 의해 생성된 값을 입력한다. algorithmKey, algorithmKeyHash 키의 노출을 피하고 싶다면 설정 파일에서 해당 항목 삭제 후 하단 [WAS VM arguments 환경 변수 등록(옵션)]을 참고하여 진행한다.

예:

<egov-crypto:config id="egovCryptoConfig"
	initial="true"
	crypto="true"
	algorithm="SHA-256"
	algorithmKey="(생성값)"
	algorithmKeyHash="(생성값)"
	cryptoBlockSize="1024"
/>

속성 설명

속성설명필수여부비고
initialglobals.properties 연계 Url, UserName, Password 값 로드 여부(true, false)필수
crypto계정 암호화 여부(true, false)필수
algorithm계정 암호화 알고리즘필수
algorithmKey계정 암호화키 키필수
algorithmKeyHash계정 암호화 키 해쉬값필수
cryptoBlockSize계정 암호화키 블록사이즈필수
cryptoPropertyLocation설정파일 암복호화 경로선택default=“classpath:/egovframework/egovProps/globals.properties”

Crypto algorithmKey, algorithmKeyHash 생성

Crypto Config 설정에 algorithmKey, algorithmKeyHash 인코딩 키 생성 방법을 제공한다. 하단 코드에서 계정암호화키 키 값을 원하는 값으로 설정한다.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.egovframe.rte.fdl.cryptography.EgovPasswordEncoder;
 
public class EgovEnvCryptoAlgorithmCreateTest {
 
	private static final Logger LOGGER = LoggerFactory.getLogger(EgovEnvCryptoAlgorithmCreateTest.class);
 
	//계정암호화키 키
	public String algorithmKey = "(사용자정의 값)";
 
	//계정암호화 알고리즘(MD5, SHA-1, SHA-256)
	public String algorithm = "SHA-256";
 
	//계정암호화키 블럭사이즈
	public int algorithmBlockSize = 1024;
 
	public static void main(String[] args) {
		EgovEnvCryptoAlgorithmCreateTest cryptoTest = new EgovEnvCryptoAlgorithmCreateTest();
 
		EgovPasswordEncoder egovPasswordEncoder = new EgovPasswordEncoder();
		egovPasswordEncoder.setAlgorithm(cryptoTest.algorithm);
 
		LOGGER.info("------------------------------------------------------");
		LOGGER.info("알고리즘(algorithm) : "+cryptoTest.algorithm);
		LOGGER.info("알고리즘 키(algorithmKey) : "+cryptoTest.algorithmKey);
		LOGGER.info("알고리즘 키 Hash(algorithmKeyHash) : "+egovPasswordEncoder.encryptPassword(cryptoTest.algorithmKey));
		LOGGER.info("알고리즘 블럭사이즈(algorithmBlockSize)  :"+cryptoTest.algorithmBlockSize);
	}
}

환경설정 파일(globals.properties) 암호화

환경설정 파일(globals.properties)의 데이터베이스 연결 항목(Url, UserName, Password) 인코딩 값 생성

환경설정 파일에서 데이터베이스 연결 항목(Url, UserName, Password) 인코딩 키 생성 방법을 제공한다.

<!-- EgovEnvCryptoUserTest.java 설정 파일 -->
<!-- context-crypto-test.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:egov-crypto="http://maven.egovframe.go.kr/schema/egov-crypto"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://maven.egovframe.go.kr/schema/egov-crypto http://maven.egovframe.go.kr/schema/egov-crypto/egov-crypto-4.1.0.xsd">
 
<bean name="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
	<property name="useCodeAsDefaultMessage">
		<value>true</value>
	</property>
	<property name="basenames">
		<list>
			<value>classpath:/egovframework/egovProps/globals</value>
		</list>
	</property>
</bean>
 
<egov-crypto:config id="egovCryptoConfig"
	initial="false"
	crypto="true"
	algorithm="SHA-256"
	algorithmKey="(사용자정의 값)"
	algorithmKeyHash="(생성값)"
	cryptoBlockSize="1024"
/>
</beans>
// 데이터베이스 연결 항목(Url, UserName, Password) 인코딩 값 생성 JAVA
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
import org.egovframe.rte.fdl.cryptography.EgovEnvCryptoService;
import org.egovframe.rte.fdl.cryptography.impl.EgovEnvCryptoServiceImpl;
 
public class EgovEnvCryptoUserTest {
 
	private static final Logger LOGGER = LoggerFactory.getLogger(EgovEnvCryptoUserTest.class);
 
	public static void main(String[] args) {
 
		String[] arrCryptoString = { 
			"userId",         //데이터베이스 접속 계정 설정
			"userPassword",   //데이터베이스 접속 패드워드 설정
			"url",            //데이터베이스 접속 주소 설정
			"databaseDriver"  //데이터베이스 드라이버
		};
 
		LOGGER.info("------------------------------------------------------");		
		ApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"classpath:/context-crypto-test.xml"});
		EgovEnvCryptoService cryptoService = context.getBean(EgovEnvCryptoServiceImpl.class);
		LOGGER.info("------------------------------------------------------");
 
		String label = "";
		try {
			for(int i=0; i < arrCryptoString.length; i++) {		
				if(i==0)label = "사용자 아이디";
				if(i==1)label = "사용자 비밀번호";
				if(i==2)label = "접속 주소";
				if(i==3)label = "데이터 베이스 드라이버";
				LOGGER.info(label+" 원본(orignal):" + arrCryptoString[i]);
				LOGGER.info(label+" 인코딩(encrypted):" + cryptoService.encrypt(arrCryptoString[i]));
				LOGGER.info("------------------------------------------------------");
			}
		} catch (IllegalArgumentException e) {
			LOGGER.error("["+e.getClass()+"] IllegalArgumentException : " + e.getMessage());
		} catch (Exception e) {
			LOGGER.error("["+e.getClass()+"] Exception : " + e.getMessage());
		}
 
	}
 
}

환경설정 파일(globals.properties) 인코딩 값 설정

  • 앞에서 생성된 데이터베이스 연결 항목(Url, UserName, Password)을 (생성값)에 입력
......
 
#mysql
Globals.mysql.DriverClassName = (생성값)
Globals.mysql.Url = (생성값)
Globals.mysql.UserName = (생성값)
Globals.mysql.Password = (생성값)
 
......

context-datasource.xml 파일의 데이터베이스 연결 항목(Url, UserName, Password) 디코딩 연결 설정

데이터베이스 설정 파일에서 데이터베이스 연결 항목을 디코딩 하는 설정 방법을 제공한다.

<?xml version="1.0" encoding="UTF-8"?>
<beans 
	xmlns="http://www.springframework.org/schema/beans" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:util="http://www.springframework.org/schema/util" 
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd 
	http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
 
......
 
	<!-- MySQL -->
	<beans profile="mysql">  
	<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
		<property name="driverClassName" value="#{egovEnvCryptoService.decrypt('${Globals.mysql.DriverClassName}')}"/>
		<property name="url" value="#{egovEnvCryptoService.getUrl()}" />
		<property name="username" value="#{egovEnvCryptoService.getUsername()}" />
		<property name="password" value="#{egovEnvCryptoService.getPassword()}" />
	</bean>
	</beans>
 
......

WAS VM arguments 환경 변수 등록(옵션)

  • 간소화 설정 파일에서 algorithmKey, algorithmKeyHash 키의 노출을 피하고 싶다면
    • 간소화 설정 파일에서 algorithmKey, algorithmKeyHash 키를 삭제 하고 WAS VM arguments 환경 변수를 등록 한다.
    • globals.properties 설정 파일의 중요 정보 Url, UserName, Password 항목에 대해 암/복호화 처리 할 수 있도록 제공한다.
-Degov.crypto.algorithmKey="(사용자정의 값)" -Degov.crypto.algorithmKeyHash="(생성값)"

2.27 - FTP Service

Apache Commons Net을 사용하여 FTP 서비스를 제공하며, FTP 클라이언트를 통해 파일 업로드, 다운로드, 삭제, 디렉토리 생성 등의 기능을 구현할 수 있다. FTP 사용 예제 및 파일 전송 설정 방법을 함께 제공한다.

FTP Service

개요

전자정부 표준프레임워크FTP 서비스제공을 위해 Apache Commons Net™ [단순 클라이언트측의 기본적인 Internet Protocol의 구현의 FTP기능을 편리하게 제공]을 오픈 소스로 채택하였다.

Apache Commons Net™은 Network utility collection 이다. Apache Commons Net™은 단순 클라이언트측의 기본적인 Internet Protocol을 구현함으로서 기본적인 프로토콜 access가 목적이기 때문에 부분적으로 object-orient 규칙에 위배가 되는 사항이 있다는 것을 참고적으로 알고 있어야 한다.

FTP정의

FTP란 FTP (File Transfer Protocol) 파일 전송 프로토콜로 FTP[에프 티 피]는 인터넷상의 컴퓨터들 간에 파일을 교환하기 위한 표준 프로토콜로서 가장 간단한 방법이기도 하다.
화면에 표시할 수 있는 웹 페이지와 관련 파일들을 전송하는 HTTP, 전자우편을 전송하는 SMTP 등과 같이, FTP 역시 인터넷의 TCP/IP 응용 프로토콜 중의 하나이다.
FTP는 웹 페이지 파일들을 인터넷상에서 모든 사람이 볼 수 있도록 하기 위해 저작자의 컴퓨터로부터 서버로 옮기는 과정에서 사용된다.
또한, 다른 서버들로부터 자신의 컴퓨터로 프로그램이나 파일들을 다운로드 하는 데에도 많이 사용된다. 
사용자 입장에서는 간단한 명령을 이용하여 FTP를 쓰거나, 또는 그래픽 사용자 인터페이스를 제공하는 상용 프로그램을 쓸 수도 있다. 
보통은 웹 브라우저도 웹 페이지로부터 선택한 프로그램을 다운로드 하는데 FTP를 사용한다. 
FTP를 사용하여 서버에 있는 파일을 지우거나 이름을 바꾸거나 옮기거나 복사하는 등 갱신작업을 할 수도 있다. 
FTP 서버에는 로그온을 해야하지만, 익명의 FTP를 사용하여 모든 사람들에게 공개된 파일들을 쉽게 접근할 수 있도록 하고 있다. 
FTP는 보통 TCP/IP에 함께 제공되는 일련의 프로그램 속에 포함되어 있다.

설명

Apache Commons Net™

Apache Commons Net™ 프로젝트(http://commons.apache.org/net/)에서 지원하는 프로토콜은 다음과 같다.

  • FTP/FTPS
  • FTP over HTTP (experimental)
  • NNTP
  • SMTP(S)
  • POP3(S)
  • IMAP(S)
  • Telnet
  • TFTP
  • Finger
  • Whois
  • rexec/rcmd/rlogin
  • Time (rdate) and Daytime
  • Echo
  • Discard
  • NTP/SNTP

Apache Commons Net - org.apache.commons.net.ftp 의 동작흐름에 대하여 간략히 설명한다.

논리적 흐름도는 아래와 같다.

1. FTP Client를 생성
2. FTP Server에 Connect 서버에 연결한다
3. 응답이 정상적인지 확인한다.
4. FTP Server 로그인한다
5. 접속하여 여러가지 작업(list, get, put....등등)
6. FTP Server 로그아웃한다
7. FTP Server disconnect

다음은 사용예제는 FTP에 접속하여 리스트를 볼수 있는 예제이다.

사용예제

private static FileInputStream inputStream;
 
public static void main(String[] args) {
 
	FTPClient client = null;
 
	// 계정 로그인
	try {
		client = new FTPClient();
 
		// 한글파일명 때문에 디폴트 인코딩을 euc-kr로 한다.
		client.setControlEncoding("euc-kr");
 
		// Test 서버 정보
		logger.info("Commons NET FTP Client Test Program");
		logger.info("Start GO");
 
		// Novell TEST서버에 접속
		client.connect("ftp.novell.com");
		logger.info("Connected to ||||||||||||||||||||||...........");
 
		// 응답코드가 비정상일 경우 종료함
		int reply = client.getReplyCode();
		if (!FTPReply.isPositiveCompletion(reply)) {
			client.disconnect();
			logger.info("FTP server refused connection");
 
		} else {
			logger.info(client.getReplyString());
 
			// timeout을 설정
			client.setSoTimeout(10000);
			// 로그인
			client.login("anonymous", "anonymous");
			logger.info("anonymous login success...");
 
 
			// 각종 정보를 처리 (Put / Get / Append등)
 
 
			client.logout();
		}
 
	} catch (Exception e) {
		logger.info("해당 ftp 로그인 실패하였습니다.");
		e.printStackTrace();
		System.exit(-1);
	} finally {
		if(client != null && client.isConnected()){
			try {
				client.disconnect();
			}catch(IOException ioe) {
				ioe.printStackTrace();
			}
		}
	}
}

파일 리스트 보기

FTPFile[] ftpfiles = client.listFiles("/");
 
if (ftpfiles != null ) {
	for (int i = 0; i < ftpfiles.length; i++) {
		FTPFile file = ftpfiles[i];
		logger.info(file.toString()); // 파일정보
		logger.info(file.getName()); // 파일명 
		logger.info(file.getSize()); // 파일사이즈
	}
}

파일 다운로드 (get)

File get_file = new File("c:\\temp\\test.jpg");
FileOutputStream outputstream = new FileOutputStream(get_file);
boolean result = client.retrieveFile("/public/test.jpg", outputstream);
 
outputstream.close();

파일 업로드 (put)

File put_file = new File("c:\\temp\\test.jpg");
inputStream = new FileInputStream(put_file);
boolean result = client.storeFile("/public/test.jpg", inputStream);
 
inputStream.close();

파일 업로드 (append)

File append_file = new File("c:\\temp\\test.jpg");
inputStream = new FileInputStream(append_file);
boolean result = client.appendFile("/public/test.jpg", inputStream);
 
inputStream.close();

파일 이름변경 (rename)

boolean result = client.rename("/public/바꾸기전.jpg", "/public/바꾼후.jpg");

파일 삭제 (delete)

boolean result = client.deleteFile("/public/삭제할.jpg");

Dircetory 생성

boolean result = client.makeDirectory("/public/test");

OS 커맨드 입력하기

client.sendCommand(FTPCommand.MAKE_DIRECTORY,"/public/test");

파일 및 전송상태 설정

  • 파일 타입 : FTP.BINARY_FILE_TYPE, FTP.ASCII_FILE_TYPE, 등 설정 - 파일 전송 형태 : FTP.STREAM_TRANSFER_MODE, COMPRESSED_TRANSFER_MODE 등 설정
/* 파일 타입*/
client.setFileType(FTP.BINARY_FILE_TYPE);
/* 파일 전송 형태 */
client.setFileTransferMode(FTP.COMPRESSED_TRANSFER_MODE);
/* Mail - IMAP[S] Client 사용 */
public final class IMAPMail {
  public static void main(String[] args) {
      if (args.length < 3) {
          System.err.println(
              "Usage: IMAPMail <imap server hostname> <username> <password> [TLS]");
          System.exit(1);
      }
      String server = args[0];
      String username = args[1];
      String password = args[2];
      String proto = (args.length > 3) ? args[3] : null;
      IMAPClient imap;
      if (proto != null) {
          System.out.println("Using secure protocol: " + proto);
          imap = new IMAPSClient(proto, true); // implicit
        
      } else {
          imap = new IMAPClient();
      }
      System.out.println("Connecting to server " + server + " on " + imap.getDefaultPort());
      imap.setDefaultTimeout(60000);
      // suppress login details
      imap.addProtocolCommandListener(new PrintCommandListener(System.out, true));
      try {
          imap.connect(server);
      } catch (IOException e) {
          throw new RuntimeException("Could not connect to server.", e);
      }
      try {
          if (!imap.login(username, password)) {
              System.err.println("Could not login to server. Check password.");
              imap.disconnect();
              System.exit(3);
          }
          imap.setSoTimeout(6000);
          imap.capability();
          imap.select("inbox");
          imap.examine("inbox");
          imap.status("inbox", new String[]{"MESSAGES"});
          imap.logout();
          imap.disconnect();
      } catch (IOException e) {
          System.out.println(imap.getReplyString());
          e.printStackTrace();
          System.exit(10);
          return;
      }
  }
}

Commons Net 2.x to Commons Net 3.0

Version 3.0의 binary는 호환이 보장 되지만 소스코드는 아래와 같은 내용이 변경되었다.

사용되지 않는 여러 상수들이 제거되었으며, binary 호환성에 영향을 주지않는 exception, public methods 들은 더이상 IOException 을 throw 하지 않도록 보완/수정되었다.

  • TelnetClient#addOptionHandler(TelnetOptionHandler)
  • TelnetClient#deleteOptionHandler(int)

참고자료

Jakarta Commons Net

2.28 - Mail 서비스

Jakarta Commons Email API를 사용하여 메일 발송을 쉽게 처리하며, 텍스트 메일, 파일 첨부, URL 첨부, HTML 메일, 인증 처리 등의 기능을 제공한다. 이를 통해 간단한 코드로 다양한 형식의 메일 발송을 구현할 수 있다.

Mail 서비스

개요

전자정부 프레임워크에서는 이메일 발송을 쉽게 처리하기 위해 Jakarta Commons Email API를 사용하고 있는데 Commons Email은 내부적으로 Java Mail API와 JavaBeans Activation API 를 제공하여 오픈 소스로 채택하였다

Apache Commons-Email은 Java Mail API를 근간으로 좀더 심플하게 메일을 보내는 방안을 제시한다.

Commons Email API는 메일 발송을 처리해주는 SimpleEmail, HtmlEmail과 같은 클래스를 제공하고 있으며, 이들 클래스를 사용하여 일반 텍스트메일, HTML 메일, 첨부 메일 등을 매우 간단(simple)하게 발송할 수 있다.

Email Sample Code는 다음과 같다.

  • 간단히 텍스트만 보내기
  • 파일 첨부하기
  • URL을 통해 첨부하기
  • HTML 이메일 보내기
  • 인증 처리하기

설명

1. 간단히 텍스트만 보내기

public class EgovSimpleMail { 
  public static void main(String args[])throws MailException {   
	  SimpleEmail email = new SimpleEmail();
	  // setHostName에 실제 메일서버정보
 
	  email.setCharset("euc-kr"); // 한글 인코딩  
 
	  email.setHostName("mail.myserver.com"); //SMTP서버 설정
	  try {
		email.addTo("jdoe@somewhere.org", "John Doe"); // 수신자 추가
	} catch (EmailException e) {
		e.printStackTrace();
	}
	  try {
		email.setFrom("me@apache.org", "Me"); // 보내는 사람
	} catch (EmailException e) {
		e.printStackTrace();
	}
	  email.setSubject("Test message"); // 메일 제목
	  email.setContent("simple 메일 Test입니다", "text/plain; charset=euc-kr");
	  try {
		email.send();
	} catch (EmailException e) {
		e.printStackTrace();
	}
  } 
}

org.apache.commons.mail.SimpleEmail 은 가장 중심이 되는 org.apache.commons.mail.Email을 상속받아 setMsg(java.lang.String msg)만을 구현한 가장 기본적인 클래스이다.

  • SMTP서버 지정 : setHostName(java.lang.String aHostName)
  • 받는 사람의 메일주소 : addTo(java.lang.String email) or addTo(java.lang.String email, java.lang.String name)
  • 보내는 사람의 메일주소 : setFrom(java.lang.String email) or setFrom(java.lang.String email, java.lang.String name)
  • 여러 사람에게 메일을 보낼 경우 : addTo 함수의 추가

setSubject(java.lang.String subject)와 setMsg(java.lang.String msg)로 메일 제목과 내용을 입력한 후 send() 함수로 전송한다.

email.setCharset(“euc-kr”)을 사용해서 메일의 캐릭터셋이 euc-kr 이라고 지정하고 있는데, 이를 표시하지 않으면 메일 제목이나 보내는 사람 이름등에 있는 한글이 깨지게 되니 주의해야한다.

2. 파일 첨부하기

public class EgovEmailAttachment {
	public static void main(String args[]) throws MailException {
 
		try {
		  // 첨부할 attachment 정보를 생성합니다
		  EmailAttachment attachment = new EmailAttachment();
		  attachment.setPath("C:\\xxxx.jpg");
		  attachment.setDisposition(EmailAttachment.ATTACHMENT);
		  attachment.setDescription("첨부 관련 TEST입니다");
		  attachment.setName("xxxx.jpg"); // 
 
		  // 기본 메일 정보를 생성합니다
		  MultiPartEmail email = new MultiPartEmail();
		  email.setCharset("euc-kr");// 한글 인코딩
		  email.setHostName("mail.myserver.com");
		  email.addTo("egov@egov.org", "전자정부"); 
		  email.setFrom("egovto@egov.org", "Me");
		  email.setSubject("전자 정부 첨부 파일 TEST입니다");
		  email.setMsg("여기는 첨부관련 내용을 입력합니다");
 
		  // 생성한 attachment를 추가합니다
		  email.attach(attachment);
 
		  // 메일을 전송합니다
		  email.send();
 
		} catch (EmailException e) {
			e.printStackTrace();
		}
	}
}

첨부파일을 보낼려면 org.apache.commons.mail.EmailAttachment 클래스와 org.apache.commons.mail.MultiPartEmail 이메일을 사용하면 된다.

파일 경로와 파일 설명등을 추가하여 setName(java.lang.String name)을 통해 첨부되는 파일명을 설정한다. 그후 MultiPartEmail 을 통해 SimpleEmail 처럼 기본 메일정보를 설정한다.

그리고 MultiPartEmail의 attach() 함수를 통해 첨부 파일을 추가하여 전송한다.

만약 첨부파일이 두개 이상이라면 EmailAttachment 를 여러개 생성하여 파일 정보를 설정 한 후 attach()를 통해 추가해 준다.

EmailAttachment 객체를 생성한 뒤 email.attach()를 사용해서 첨부할 파일을 추가해주기만 하면 된다. 실제 파일명은 한글이 포함되더라도, EmailAttachment.setName() 메소드를 사용해서 파일명을 변경해서 전송할 수도 있다. 주의할 점은 1.0 버전의 Commons Email은 파일명을 한글로 전달할 경우, 파일명이 올바르게 전달되지 않고 깨져서 간단하는 점이다. (파일 자체는 올바르게 전송된다.) 따라서 1.0 버전의 Common Email을 사용하여 파일을 전송할 때에는 알파벳과 숫자로만 구성된 이름의 파일을 전송한다.

3. URL을 통해 첨부하기

public class EgovEmailAttachmentUrl {
	public static void main(String args[]) throws MailException, MalformedURLException {
		try {
		  // 첨부할 URL정보 및 파일 기본 정보를 설정합니다
		  EmailAttachment attachment = new EmailAttachment();
		  attachment.setURL(new URL("http://www.apache.org/images/asf_logo_wide.gif"));
		  attachment.setDisposition(EmailAttachment.ATTACHMENT);
		  attachment.setDescription("Apache logo");
		  attachment.setName("Apache logo");
 
		  // 기본 메일 정보를 생성합니다
		  MultiPartEmail email = new MultiPartEmail();
		  email.setHostName("mail.myserver.com");
		  email.addTo("jdoe@somewhere.org", "John Doe");
		  email.setFrom("me@apache.org", "Me");
		  email.setSubject("The logo");
		  email.setMsg("Here is Apache's logo");
 
		  // attachment를 추가합니다
		  email.attach(attachment);
 
		  // 메일을 전송합니다
		  email.send();
 
		} catch (EmailException e) {
			e.printStackTrace();
		}
	}
}

파일 경로 정보를 setURL(java.net.URL) 으로 설정할 뿐 위의 첨부파일과 동일하다.

4. HTML 이메일 보내기

public class EgovHtmlEmailSend {
	public static void main(String args[]) throws MailException, MalformedURLException {
		// 기본 메일 정보를 생성합니다
		try {
		HtmlEmail email = new HtmlEmail();
		email.setHostName("mail.myserver.com");
		email.addTo("xxxx@somewhere.org", "xxxx");
		email.setFrom("me@apache.org", "Me");
		email.setSubject("Test email with inline image");
 
		// 삽입할 이미지와 그 Content Id를 설정합니다
		URL url = new URL("http://www.apache.org/images/asf_logo_wide.gif");
		String cid = email.embed(url, "Apache logo");
 
		// HTML 메세지를 설정합니다
		email.setHtmlMsg("<html>The apache logo - <img src=\"cid:"+cid+"\"></html>");
 
		// HTML 이메일을 지원하지 않는 클라이언트라면 다음 메세지를 뿌려웁니다
		email.setTextMsg("Your email client does not support HTML messages");
 
		// 메일을 전송합니다
		email.send();
 
		} catch (EmailException e) {
        	e.printStackTrace();
        }
	}
}

email.setHtmlMsg()를 사용하여 메일 내용을 입력할 때 캐릭터셋을 별도로 지정하지 않아도 한글이 깨지지 않는다.

5.인증처리

만약 SMTP 서버가 인증을 요구한다면 org.apache.commons.mail.Email 의 setAuthentication(java.lang.String username, java.lang.String password)를 통해 해결할 수 있다

이 함수는 JavaMail API의 DefaultAuthenticator 클래스를 생성하여 사용한다.

SimpleEmail email = new SimpleEmail();
email.setCharset("euc-kr");
email.setHostName("mail.somehost.com");
email.setAuthentication("madvirus", "password");
...

예제 Sample 실행

  1. utilappSample.zip(egovframework.rex.utilappsample.zip) 파일을 다운로드 받는다.
  2. 이클립스에서 다운로드 받은 폴더를 선택하여 프로젝트를 Import 한다.
  3. lib에 라이브러리 파일이 있는지 확인한다. src 폴더 아래에 Index.jsp를 선택하여 마우스 오른쪽 클릭하여 Run As > Run On Server를 실행한다.
  4. Console 창에서 정상적으로 Tomcat이 실행된 것을 확인한다.
샘플 utilappSample의 Index.jsp 실행하였을 경우 브라우져에서 실행되는 화면

mail-sample-browser-sceenshot

참고 자료

Apache Commons-Email UserGuide

예제

mail-example

2.29 - Compress/Decompress Service

전자정부 표준프레임워크는 Jakarta Commons Compress를 통해 다양한 압축 방식(tar, zip, bzip2 등)을 지원하는 편리한 API를 제공한다.

Compress/Decompress Service

개요

전자정부 프레임워크에서는 다양한 압축방식을 개발자들에게 편리한 API를 제공하는 Jakarta Commons의 Compress를 오픈소스로 채택하였다.

Jakarta Commons의 Compress에서 지원하는 tar, zip and bzip2 파일등을 지원한다.

현재 Commons Compress API 에서는 아래의 Packages를 제공하고 있다.

  • org.apache.commons.compress.archivers
  • org.apache.commons.compress.archivers.ar
  • org.apache.commons.compress.archivers.cpio
  • org.apache.commons.compress.archivers.jar
  • org.apache.commons.compress.archivers.tar
  • org.apache.commons.compress.archivers.zip
  • org.apache.commons.compress.changes
  • org.apache.commons.compress.compressors
  • org.apache.commons.compress.compressors.bzip2
  • org.apache.commons.compress.compressors.gzip
  • org.apache.commons.compress.utils

보다 자세한 사항은 Commons Compress API를 참고하기 바란다.

설명

압축이란 파일에 저장되어 있는 정보를 압축하여 보다 적은 기억 공간에 동일한 정보를 저장하는 기술이다.

일반적으로 정보에 포함되어 있는 중복된 내용을 삭제하거나 보다 적은 길이의 코드를 사용하여 정보를 표현하는 방법을 사용하여 저장에 필요한 공간의 크기를 줄인다. 
이런 과정을 압축이라고 하며, 압축된 정보를 사용하기 위해서 다시 원래의 상태로 복원하는 과정을 압축해제라고 한다.
압축 방법에는 손실이 없는 압축 방법과 손실이 있는 압축 방법이 있다.
먼저 프로그램과 데이터 등과 같은 정보는 반드시 손실이 없는 압축 방법을 사용하여야 한다.
손실이 없는 압축 방법이라고 하는 것은 압축된 정보를 다시 복원한 경우에 압축되기 이전의 상태와 동일한 내용과 크기를 가지게 되는 압축 방법을 말한다.
하지만 이미지나 음성 등과 같은 경우에는 손실이 있는 압축 방법을 사용할 수 있다.
이미지나 음성에는 방대한 양의 정보가 존재하고 이들 정보 중에 일부가 사라진다 하더라도 사람이 눈이나 귀로 그 차이를 구별할 수 없기 때문이다.
손실이 있는 압축 방법이라고 하는 것은 압축된 정보를 다시 복원하더라도 압축되기 이전의 상태 혹은 크기와 동일하지 않은 내용을 가질 수 있는 압축 방법을 뜻한다.

간단히 여기서는 자주 사용하는 압축파일의 종류는 아래의 참조 링크를 참고한다.

사용 예제

다음은 테스트코드 EgovZipTestCase의 testZipArchiveCreation() 메소드로 org.apache.commons.compress.archivers 패키지에 속한 ArchiveInputStream 클래스를 사용하여 compress/decompress로 구성되어 있다.

public final class EgovZipTestCase extends EgovAbstractTestCase {
 
	/*
	 * Zip을 사용한 압축(Archive) TEST
	 * 두개의 파일(test1.xml, test2.xml)을 사용하여  압축하여 bla.zip으로 압축함
	 */
	public void testZipArchiveCreation() throws Exception {
        // Archive
        final File output = new File(dir, "bla.zip");
        final File file1 = getFile("test1.xml");
        final File file2 = getFile("test2.xml");
 
        {
            final OutputStream out = new FileOutputStream(output);
            final ArchiveOutputStream os = new ArchiveStreamFactory().createArchiveOutputStream("zip", out);
 
            os.putArchiveEntry(new ZipArchiveEntry("testdata/test1.xml"));
            IOUtils.copy(new FileInputStream(file1), os);
            os.closeArchiveEntry();
 
            os.putArchiveEntry(new ZipArchiveEntry("testdata/test2.xml"));
            IOUtils.copy(new FileInputStream(file2), os);
            os.closeArchiveEntry();
            os.close();
        }
 
        // Unarchive the same
        List results = new ArrayList();
 
        {
            final InputStream is = new FileInputStream(output);
            final ArchiveInputStream in = new ArchiveStreamFactory().createArchiveInputStream("zip", is);
 
            File result = File.createTempFile("dir-result", "");
            result.delete();
            result.mkdir();
 
            ZipArchiveEntry entry = null;
            while((entry = (ZipArchiveEntry)in.getNextEntry()) != null) {
                File outfile = new File(result.getCanonicalPath() + "/result/" + entry.getName());
                outfile.getParentFile().mkdirs();
                OutputStream out = new FileOutputStream(outfile);
                IOUtils.copy(in, out);
                out.close();
                results.add(outfile);
            }
            in.close();
        }
 
        assertEquals(results.size(), 2);
        File result = (File)results.get(0);
        assertEquals(file1.length(), result.length());
        result = (File)results.get(1);
        assertEquals(file2.length(), result.length());
    }
}

다음은 위에서 압축한 파일을 압축해제(Unarchive)하는 테스트 코드이다.

public void testZipUnarchive() throws Exception {
    final File input = getFile("bla.zip");
    final InputStream is = new FileInputStream(input);
    final ArchiveInputStream in = new ArchiveStreamFactory().createArchiveInputStream("zip", is);
    final ZipArchiveEntry entry = (ZipArchiveEntry)in.getNextEntry();
    final OutputStream out = new FileOutputStream(new File(dir, entry.getName()));
    IOUtils.copy(in, out);
    out.close();
    in.close();
}

참고자료

예제

2.30 - 압축 파일의 종류

다양한 압축 파일 형식과 그 사용 방식에 대해 설명하며, 각 형식의 특징과 관련 프로그램을 안내한다.

압축 파일의 종류

압축 파일설명
.alz이스트소프트에서 개발한 압축 형식입니다. 분할 압축을 할 경우 확장자는 (ALZip 으로 생성) alz, a00, a01…형식으로 생성됨.
.aceACE, WinAce에서 이용하는 압축 형식입니다. 분할 압축을 할 경우 확장자는 ace, c00, c01, … 형식으로 생성됨.
.arcDOS용 프로그램 pkarc.com, pkxarc.com에서 사용되는 압축 형식.
.arjDOS용 프로그램 arj.exe, 윈도우용 프로그램 WinArj에서 이용하는 압축 형식. 분할 압축을 할 경우 확장자는 arj, a01, a02,… 형식으로 생기게 됨.
.b64인터넷에서 문서를 주고 받을 때 사용하는 형식으로 BASE64MIME 형식으로 인코딩된 파일임.
.bhBinHex Format. E-Mail로 이진(binary) 형태의 파일을 보내기 위해 사용하는 형식임.
.bhx인터넷에서 문서를 주고 받을 때 사용하는 형식으로 BASE64MIME 형식으로 인코딩된 파일임.
.binMacintosh용이며, MacBinary Format. Aladdin StuffIt Expander에서 지원함.
.bz2UNIX용의 bzip2에서 사용하는 압축 형식입니다. 파일 하나만 압축할 수 있으므로 주로 .tar와 함께 사용되며 이 경우 .tar.bz2의 확장자를 갖음. (bzip2로 생성)
.cabMicrosoft Cabinet 파일. 마이크로소프트에서 사용하는 압축 형식임.
.ear내부적으로 zip 압축 알고리즘을 사용하는 파일 형식임.
.encE-Mail로 이진(binary) 형태의 파일을 보내기 위해 사용하는 형식임.
.gzUNIX용의 gzip에서 사용하는 압축 형식. 파일 하나만 압축할 수 있으므로 주로 .tar와 함께 사용되며 이 경우 .tar.gz의 확장자를 갖고 줄여서 .tgz 확장자를 사용하기도 함.
.haPPMC를 개선한 압축 파일 형식.
.hqx맥에서 제작된 파일을 인터넷에서 문서를 주고 받을 때 사용하는 형식임. (BinHex로 생성)
.iceDOS용 프로그램 ice.exe에서 사용하는 압축 형식임. 실제 파일 내용은 lha와 동일함.
.imgDisk image를 저장해둔 파일로, Falk Huth에 의해 만들어진 img.exe라는 프로그램을 이용하여 파일들을 추출해 낼 수 있음.
.jar자바의 jar.exe에서 사용하는 압축 형식. 내부적으로 zip 압축 알고리즘을 사용함.
.lha, .lzhDOS용 프로그램 lha.exe, lharc.exe에서 사용하던 압축 형식. Lempel-Ziv 알고리즘을 사용함.
.mim인터넷에서 문서를 주고 받을 때 사용하는 형식.
.pakDOS용 프로그램 pak.exe에서 사용하던 압축 형식.
.rarDOS용 프로그램 rar.exe와 윈도우용 프로그램 winrar.exe에서 사용하는 압축 형식. 분할 압축을 할 경우 확장자는 rar, r00, r01,… 형식으로 생기게 됨.
.sitMacintosh에서 이용되는 압축 Format. WinArj, Aladdin StuffIt Expander, WinPack 등의 윈도우용 압축 프로그램에서 이를 지원함.
.tarUNIX 명령 tar를 이용해 생성되는 파일 형식으로 실제로는 압축은 되지 않고 여러 파일을 하나로 묶어주기만 함. 보통 tar로 묶은 후 gz으로 압축하며 이 경우 .tar.gz의 확장자를 갖고 줄여서 .tgz 확장자를 사용하기도 함.
.tgzUNIX에서 tar로 묶은 파일을 gzip으로 압축한 파일 형식.
.uueUU Encoded Format. E-Mail로 이진(binary) 형태의 파일을 보내기 위해 사용하는 형식.
.war내부적으로 zip 압축 알고리즘을 사용하는 파일 형식.
.xxeXXEncode Format. E-Mail로 이진(binary) 형태의 파일을 보내기 위해 사용하는 형식.
.zUNIX용의 compress, uncompress에서 사용하는 파일 형식.
.zip도스용 프로그램 pkzip.exe, pkunzip.exe에서 사용하는 파일 형식.
.zooDOS용 프로그램 zoo.exe에서 사용되는 파일 형식.
.001rzjoin으로 분할된 파일 형식. 압축이 아닌 단순히 001, 002…… 순서로만 분할된 파일.

2.31 - File Upload/Download 서비스

Commons FileUpload를 사용해 파일 업로드 API를 제공하며, 멀티 파일 업로드 문제와 해결 방법을 설명한다.

File Upload/Download 서비스

개요

전자정부 프레임워크에서는 다양한 파일 업로드 API를 제공하는 Commons FileUpload를 오픈 소스로 채택하였다.

Spring 에서는 Commons FileUpload를 사용하여 싱글 파일 업로드에 대하여 가이드 하고 있다. 현재 Spring에서 싱글 파일 업로드에 대해서 매우 좋은 api를 제공해주고 있으나 멀티플 파일 업로드시에 동일한 이름의 여러 개의 파일을 올리려고 할 때 오류가 발생한다.

오류 사항에 대해서는 multipart multi file upload 지원 문제를 참고.

본 매뉴얼에서는 싱글 파일 업로드 보다 멀티플 파일 업로드를 가능하도록 그 대안에 대하여 설명하고자 한다.

설명

데이터 전송방식

데이터를 전송하는 방식에는 GET 방식과 POST 방식, 그리고 ENCTYPE 속성의 “multipart/form-data"이 있다.

  • GET
    • URL에 폼데이터가 노출되기 때문에 입력 내용의 길이 제한이 있고 256byte~4096byte 까지의 데이터를 전송할 수 있다.
  • POST
    • URL에 노출되지 않고 데이터를 전송하기 때문에 입력 내용의 길이에 제한을 받지 않는다.

이렇게 데이터를 전송하는데 아무 문제 없을 것 처럼 보이지만, 이 둘은 보낼 수 있는 데이터 양에 한계가 있다.

파일이나 용량이 큰 데이터를 전송할 때 문제가 생기는 것이다.

그 때 쓰는 폼 데이터 전송방식이 바로 ENCTYPE 속성의 multipart/form-data 이다.

전자정부 프레임워크에서는 스프링에서 제공하는 Apache Commons FileUpload API를 이용하여 파일 업로드를 처리하는 CommonsMultipartResolver 클래스를 제공하고 다음과 같이 설정파일에 CommonsMultipartResolver를 빈으로 등록하여 준다.

Apache Commens FileUpload 에서 싱글 파일 업로드에 대해서 매우 좋은 api를 제공해주고 있다. 하지만 멀티플 파일 업로드시 동일한 이름의 여러 개의 파일을 올리려고 할 때 오류가 발생한다.

여러 개의 파일을 올리려고 할 때 오류가 발생하는 문제에 대해서는 multipart multi file upload 지원 문제를 참고.

Spring에서 multipart를 사용한 파일 업로드에 대해서는 Spring’s multipart (fileupload) support 에서 자세하게 가이드 하였으므로 본 메뉴얼에서는 다루지 않는다.

기능 실행에 대한 이해를 돕기 위해 컨텐츠와 함께 컨텐츠에서 제시한 샘플 코드를 포함하고 있는 이클립스 프로젝트 형태의 웹 애플리케이션 샘플 프로젝트를 다운로드할 수 있다.

File Upload / Download

File Upload / Download 에 대한 설명은 아래 상세 페이지를 참고하라.

예제 Sample 실행

  1. utilappSample.zip(utilappsample.zip) 파일을 다운로드 받는다.
  2. 이클립스에서 다운로드 받은 폴더를 선택하여 프로젝트를 Import 한다.
  3. lib에 라이브러리 파일이 있는지 확인한다.
  4. src 폴더 아래에 Index.jsp를 선택하여 마우스 오른쪽 클릭하여 Run As > Run On Server를 실행한다.
  5. Console 창에서 정상적으로 Tomcat이 실행된 것을 확인한다.
  6. 본 샘플에서는 CommonsMultipartResolver를 보완한 MultiCommonsMultipartResolver를 사용하고 있으나 CommonsMultipartResolver 사용을 권장함.

multiple files with a single file(한단 샘플)에서 사용한 JavaScript를 사용하여 추가 시 다른 form name을 추가하여 처리하는 방식을 가이드 하였으니 참고하기 바란다.

샘플 utilappSample의 Index.jsp 실행하였을 경우 브라우저에서 실행되는 화면

file-updown-service-sample-screenshot

참고자료

예제

2.32 - File Upload Service

파일 업로드를 위한 Spring의 CommonsMultipartResolver 설정 및 구현 방법을 설명하며, JSP 폼과 컨트롤러를 통해 파일을 업로드하는 예제를 제공한다. 파일 저장 경로는 properties 파일에서 설정하며, 다중 파일 업로드 시 폼 이름을 다르게 설정해야 한다.

File Upload Service

개요

업로드는 한 컴퓨터 시스템에서 다른 시스템으로 파일을 전송하는 것을 말하는데, 대개 작은 컴퓨터에서 큰 컴퓨터로 옮길 때 이런 용어를 사용한다. 네트웍 사용자의 관점에서 보면, 파일을 업로드하는 것은 그 파일을 받을 수 있도록 설정된 다른 컴퓨터에 파일을 보내는 것이다. 전자게시판 상의 다른 사용자와 이미지 파일을 공유하기를 원하는 사람들은 그 전자게시판에 파일을 업로드하면 된다.

그러면 반대편 입장에 있는 사람은 그 파일을 다운로드하게 되는데, 여기서 다운로드는 대개 큰 컴퓨터에서 작은 컴퓨터로 파일을 전송하는 것을 의미한다. 인터넷 사용자의 입장에서의 다운로드란 다른 컴퓨터에서 파일을 받는 것이다.

설명

파일 업로드 기능을 구현하기 위해서는 먼저 빈 설정 파일에 다음과 같이 MultiCommonsMultipartResolver를 정의해야한다. 본 가이드에서는 Apache Commons FileUpload에서 재공하는 CommonsMultipartResolver를 사용하기를 권장한다. CommonsMultipartResolver를 수정하여 사용할 경우 많은 부분에 시간과 노력이 들어갈 것이다.

<!-- Custom MultiFile resolver -->
<bean id="local.MultiCommonsMultipartResolver"
	class="egovframework.rte.util.web.resolver.MultiCommonsMultipartResolver">
	<property name="maxUploadSize" value="100000000" />
	<property name="maxInMemorySize" value="100000000" />
</bean>

[권장]만약 스프링에서 제공하는 Apache Commons FileUpload API를 이용하여 파일 업로드를 처리하는 CommonsMultipartResolver를 사용하려고 하면 빈 설정파일에 다음과 같이 정의한다.

<!-- MULTIPART RESOLVERS -->
<!-- regular spring resolver -->
<bean id="spring.RegularCommonsMultipartResolver"
	class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
	<property name="maxUploadSize" value="100000000" />
	<property name="maxInMemorySize" value="100000000" />
</bean>

또한 해당 컨트롤러의 property로 파일의 업로드 위치를 지정해주고 컨트롤러에서 setter 메소드를 통해 지정된 파일 업로드 위치를 불러올 수 있다. 사용예는 다음과 같다.

fileUploadProperties.properties

# windows NT일 경우  
file.upload.path=C:\\temp
 
# Unix일 경우
file.upload.path=/usr/file/upload
@Resource(name = "fileUploadProperties")
Properties fileUploadProperties;
..
..
..
	String uploadPath = fileUploadProperties
			.getProperty("file.upload.path");
	File saveFolder = new File(uploadPath);
..
..
..

파일 업로드를 위해 JSP파일의 입력 폼 타입을 file로 지정하고 form의 enctype을 multipart/form-data로 지정한다. 예시는 다음과 같다. 이때 CommonsMultipartResolver를 사용할 경우 form의 name을 다르게 설정하여야 한다. 중복된 이름을 사용할 경우 에러가 발생한다.

<form method="post" action="<c:url value='/upload/genericMulti.do'/>"
enctype="multipart/form-data">
  <p>Type:
    <input type="text" name="type" value="genericFileMulti" size="60" />
  </p>
  <p>File1:
    <input type="file" name="file[]" size="60" />
  </p>
  <p>File2:
    <input type="file" name="file[]" size="60" />
  </p>
  <p>
    <input type="submit" value="Upload" />
  </p>
</form>

다음은 파일 업로드를 위해 Controller를 구현한 모습이다.

@Controller("genericFileUploadController")
public class GenericFileUploadController {
 
@Resource(name = "multipartResolver")
CommonsMultipartResolver multipartResolver;
 
@Resource(name = "fileUploadProperties")
Properties fileUploadProperties;
 
@SuppressWarnings("unchecked")
@RequestMapping(value = "/upload/genericMulti.do")
public String multipartProcess(final HttpServletRequest request, Model model)
	throws Exception {
 
final long startTime = System.nanoTime();
 
/*
 * validate request type
 */
Assert.state(request instanceof MultipartHttpServletRequest,
		"request !instanceof MultipartHttpServletRequest");
final MultipartHttpServletRequest multiRequest = (MultipartHttpServletRequest) request;
 
/*
 * validate text input
 */
Assert.state(request.getParameter("type").equals("genericFileMulti"),
		"type != genericFileMulti");
 
/*
 * extract files
 */
final Map<String, MultipartFile> files = multiRequest.getFileMap();
Assert.notNull(files, "files is null");
Assert.state(files.size() > 0, "0 files exist");
 
/*
 * process files
 */
String uploadPath = fileUploadProperties
		.getProperty("file.upload.path");
File saveFolder = new File(uploadPath);
 
// 디렉토리 생성
if (!saveFolder.exists() || saveFolder.isFile()) {
	saveFolder.mkdirs();
}
 
Iterator<Entry<String, MultipartFile>> itr = files.entrySet()
		.iterator();
MultipartFile file;
List fileInfoList = new ArrayList();
String filePath;
 
while (itr.hasNext()) {
	Entry<String, MultipartFile> entry = itr.next();
	System.out.println("[" + entry.getKey() + "]");
 
	file = entry.getValue();
	if (!"".equals(file.getOriginalFilename())) {
		filePath = uploadPath + "\\" + file.getOriginalFilename();
		file.transferTo(new File(filePath));
 
		FileInfoVO fileInfoVO = new FileInfoVO();
		fileInfoVO.setFilePath(filePath);
		fileInfoVO.setFileName(file.getOriginalFilename());
		fileInfoVO.setFileSize(file.getSize());
		fileInfoList.add(fileInfoVO);
	}
}
 
// 여기서는 DB에 파일관련 정보를 저장하지 않고 단순히 success 페이지로 포워딩 하여 재확인 가능토록 함
model.addAttribute("fileInfoList", fileInfoList);
model.addAttribute("uploadPath", uploadPath);
 
final long estimatedTime = System.nanoTime() - startTime;
System.out.println(estimatedTime + " " + getClass().getSimpleName());
 
return "success";
 
	}
}

Tomcat에서는 일반적으로 웹 어플리케이션이 GET과 POST 방식으로 파라미터를 넘겨 받을 때 request.setCharacterEncoding()을 통한 문자셋 인코딩이 필요하다.

예제

2.33 - Spring mvc Multipart Multi file upload 지원 문제

Spring MVC 2.5.5에서 동일한 이름의 여러 파일을 업로드할 때 발생하는 MultipartResolver 오류 문제를 설명한다.

Spring mvc Multipart Multi file upload 지원 문제

개요

Spring mvc 2.5.5 Multipart Multi file upload 지원부분에서 동일한 이름의 여러개의 파일을 올리려고 할 때 에러가 발생한다. 본 가이드에서는 이러한 문제가 발생하여 아직 Spring쪽에서 답변이 없는 상황이다. 이부분에 대하여 개발시 참고 하기바란다.

설명

업로드 갯수를 고려하지 않고 동적으로 upload 폼을 추가할 경우 오류 메시지가 나온다.

org.springframework.web.multipart.MultipartException: Multiple files for field name [files] found - not supported by MultipartResolver
org.springframework.web.multipart.commons.CommonsFileUploadSupport.parseFileItems(CommonsFileUploadSupport.java:254)
org.springframework.web.multipart.commons.CommonsMultipartResolver.parseRequest(CommonsMultipartResolver.java:166)
org.springframework.web.multipart.commons.CommonsMultipartResolver.resolveMultipart(CommonsMultipartResolver.java:149)
org.springframework.web.servlet.DispatcherServlet.checkMultipart(DispatcherServlet.java:1015)
org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:851)
org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:807)
org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:571)
org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:511)
javax.servlet.http.HttpServlet.service(HttpServlet.java:637)
javax.servlet.http.HttpServlet.service(HttpServlet.java:717)

이러한 문제에 대해서 아래의 참고 자료를 참고하기 바란다.

참고자료

  • (deplicated) Implementing single and multiple file multipart uploads using Spring 2.5
  • (deplicated) 동일한 form name upload 문제
  • (deplicated) spring mvc 2.5.5 multipart multi file upload 지원

2.34 - File Download Service

파일 다운로드 기능을 구현한 DownloadController 예제와 JSP 페이지 설정을 설명한다.

File Download Service

개요

여기서 다운로드는 대개 큰 컴퓨터에서 작은 컴퓨터로 파일을 전송하는 것을 의미한다. 인터넷 사용자의 입장에서의 다운로드란 다른 컴퓨터에서 파일을 받는 것이다.

설명

EgovFrameWork에서는 파일 다운로드를 하기위한 DownloadController 클래스를 간단하게 구현하여 보았다.

DownloadController 클래스 예시

@Controller("downloadController")
public class DownloadController {
 
	@Resource(name = "fileUploadProperties")
	Properties fileUploadProperties;
 
	@RequestMapping(value = "/download/downloadFile.do")
	public void downloadFile(
			@RequestParam(value = "requestedFile") String requestedFile,
			HttpServletResponse response) throws Exception {
 
		String uploadPath = fileUploadProperties
				.getProperty("file.upload.path");
 
		File uFile = new File(uploadPath, requestedFile);
		int fSize = (int) uFile.length();
 
		if (fSize > 0) {
 
			BufferedInputStream in = new BufferedInputStream(
					new FileInputStream(uFile));
			// String mimetype = servletContext.getMimeType(requestedFile);
			String mimetype = "text/html";
 
			response.setBufferSize(fSize);d
			response.setContentType(mimetype);
			response.setHeader("Content-Disposition", "attachment; filename=\""
					+ requestedFile + "\"");
			response.setContentLength(fSize);
 
			FileCopyUtils.copy(in, response.getOutputStream());
			in.close();
			response.getOutputStream().flush();
			response.getOutputStream().close();
		} else {
			//setContentType을 프로젝트 환경에 맞추어 변경
			response.setContentType("application/x-msdownload");
			PrintWriter printwriter = response.getWriter();
			printwriter.println("<html>");
			printwriter.println("<br><br><br><h2>Could not get file name:<br>"
					+ requestedFile + "</h2>");
			printwriter
					.println("<br><br><br><center><h3><a href='javascript: history.go(-1)'>Back</a></h3></center>");
			printwriter.println("<br><br><br>&copy; webAccess");
			printwriter.println("</html>");
			printwriter.flush();
			printwriter.close();
		}
	}
}

jsp 페이지로 간단하게 구현된 예시

<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Success</title>
</head>
 
<body>
 
<h1>Success</h1>
 
<p>All good</p>
 
<c:forEach var="file" items="${fileInfoList}" varStatus="status">
 
순번 : <c:out value="${status.count}" />
	<br />
uploadedFilePath : <c:out value="${file.filePath}" />
	<br />
파일명 : <a
		href="#" onclick="window.open(encodeURI('<c:url value='/download/downloadFile.do?'/>requestedFile=${file.fileName}'))"><c:out
		value="${file.fileName}" /></a>
	<br />
파일사이즈 : <c:out value="${file.fileSize}" />
	<br />
	<p />
</c:forEach>
 
</body>
</html>

Tomcat에서는 일반적으로 웹 어플리케이션이 GET과 POST 방식으로 파라미터를 넘겨 받을 때 request.setCharacterEncoding()을 통한 문자셋 인코딩이 필요하다.

파일을 다운로드시 한글이 깨지는 문제가 발생할 경우 제우스나 WebLogic의 경우는 JSP 페이지에 아래와 같이 넣어주면 한글 깨지는 문제가 해결된다.

<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>

Tomcat에서는 한글이 깨지는 문제가 발생하는데 아래의 링크를 참조

2.35 - Tomcat 한글 설정하기

Tomcat에서 문자셋 인코딩을 하여 한글이 깨지는 문제를 해결할 수 있다.

Tomcat 한글 설정하기

개요

Tomcat에서 문자셋 인코딩을 하여 한글이 깨지는 문제를 해결할 수 있다.

설명

일반적으로 웹 어플리케이션이 GET과 POST 방식으로 파라미터를 넘겨 받을 때 request.setCharacterEncoding()을 통한 문자셋 인코딩이 필요하다.

Tomcat 4.x

단순히 JSP 혹은 서블릿의 최 상단에 request.setCharacterEncoding("euc-kr");을 하면 된다. 

GET과 POST 방식에 상관없이 인코딩을 해준다. 

Tomcat 5.x

POST 방식은 request.setCharacterEncoding("euc-kr");로 계속 하면된다. 
 
하지만 GET 방식은 server.xml의 <Connector> 설정 부분을 바꿔줘야만 한다. 
 
<Connector port="8080" 
maxThreads="150" minSpareThreads="25" maxSpareThreads="75" 
enableLookups="false" redirectPort="8443" acceptCount="100" 
debug="0" connectionTimeout="20000" 
disableUploadTimeout="true" URIEncoding="euc-kr"/> 
 
위에서 URIEncoding="euc-kr" 부분이다.

결론적으로 Tomcat 4.x와 Tomcat 5.x 는 모두 request.setCharacterEncoding()이 필요하다는 사실에는 변함이 없다.

한글 파라미터를 가진 링크를 만들 때

JSP페이지에서 링크를 생성할 때, 한글이 됐든 공백이나 특수문자를 가진 영어가 됐든, 순수하게 영어와 숫자, 밑줄 등으로만 이뤄진게 아닌 모든 파라미터를 넘길 때는 무조건 URLEncoding을 해야한다고 봐도 된다.

Web Container에 따라 URLEncoding을 안하고 넘겨도 작동하는 경우가 있는데, 동일한 웹 컨테이너라도 버전에 따라 한글을 제대로 인식하지 못하는 경우도 있고, 또 다른 컨테이너에서는 URLEncoding이 안된 한글을 전혀 인식하지 못할 수도 있다.

그러므로 무조건 표준을 따라서 java.net.URLEncoder.encode()메 소드를 사용해 인코딩해서 넘기도록 한다. 디코드 작업은 request.setCharacterEncoding()에 의해서 자동으로 이뤄지므로 해줄것이 없다. (Tomcat 3.x대- JSP Spec 1.1 -에서는 request.setCharacterEncoding()이 없으므로 String.getBytes()를 이용해 직접 디코딩을 해줘야만 했다)

<%@ include file=“test.jspf”%> 에서의 한글

위와 같이 test.jspf를 static include 할 경우에 test.jspf에 있는 한글이 모두 깨질 수 있다. test.jspf에도 한글 설정이 필요한데, 이 경우에는 test.jspf의 최 상단에 다음을 추가하면 된다.

<%@page pageEncoding="euc-kr"%>
  • static include : JSP 페이지를 컴파일하는 시점에 해당 jspf 파일의 내용을 문자열 그대로 현재 jsp에 삽입하여 컴파일 하는 것. static include 방식에서 include 되는 대상 jsp의 확장자는 .jspf로 하는 것이 표준이다. .jspf 는 단독 실행을 위한 것이 아니라 항상 다른 JSP에 포함되어 쓰이는 목적으로 만들어졌기 때문에 완전한 JSP의 형태를 갖추고 있지 않다.
  • <jsp:include page=””/> 이 방식은 동적 include 방식으로, JSP 페이지가 실행되는 중간에 page에 지정된 jsp를 실행한 결과를 삽입하는 방식이다. 이 방식에서는 include 되는 JSP 페이지가 원래부터 페이지 인코딩 정보 등을 포함한 완전한 JSP 형태를 갖추고 있어야만 한다.

참고자료

2.36 - File Handling

Excel 다운로드를 위한 파일 처리 서비스를 적용한 예제 코드와 설정을 제공한다.

File Handling

개요

File Handling 서비스를 적용해서 Excel 다운로드 하기 위한 Excel 정보를 설정한다.

Excel 서비스에 적용되어 있다.

설명

Source

FileObject writtenFile = manager.resolveFile(baseDir, this.propertyPath);
FileContent writtenContents = writtenFile.getContent();
InputStream is = writtenContents.getInputStream();
 
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuffer sb = new StringBuffer();
 
for (String line = ""; (line = reader.readLine()) != null; sb.append(line));
is.close();

2.37 - Excel Service

Excel 서비스는 Apache POI를 사용해 Excel 파일 다운로드 및 업로드를 지원하며, 3.0 버전에서는 메소드 리팩토링과 MyBatis 지원을 추가했다.

Excel Service

개요

Excel 파일 포맷을 다룰 수 있는 자바 라이브러리를 제공하여, 사용자들이 데이터를 Excel 파일 포맷으로 다운받거나, 대량의 Excel 데이터를 시스템에 올릴 수 있도록 지원하기 위한 서비스이다. Excel 서비스는 Apache POI 오픈소스를 사용하여 구현하였으며 주요 Excel 접근 기능 외에 Excel 다운로드, Excel 파일 업로드 등의 기능이 있다. Excel 서비스 3.0 버전에서는 기존 버전을 refactoring 하였다. 기존의 메소드(xls, xlsx)를 지원하는 메소드들의 이름을 하나로 하여 Parameter 방식으로 구분자를 추가하였다. 또한, 기존의 iBatis 뿐만 아니라 MyBatis도 지원하는 클래스를 추가하였다.

설명

주요기능

Excel 파일 생성

엑셀 파일을 생성하여 지정된 위치에 저장하는 기능을 제공한다.

Workbook 인스턴스를 생성하여 Excel sheet를 추가 생성할 수 있다.

엑셀 버전에 따라 엑셀 97~2003버전(xls)인 HSSFWorkbook, 엑셀 2007이상(xlsx)의 XSSFWorkbook 클래스를 사용할 수 있으며, 각 클래스별 사용 법(method)는 동일하다.

Sample Source
String sheetName1 = "first sheet";
String sheetName2 = "second sheet";
StringBuffer sb = new StringBuffer();
// 엑셀 필요버전에 맞는 확장자를 선택하면 됨
sb.append(fileLocation).append("/").append("testWriteExcelFile.xls");
sb.append(fileLocation).append("/").append("testWriteExcelFile.xlsx");
 
// Workbook을 필요버전에 맞는 클래스를 선택하면 됨
Workbook wb = new HSSFWorkbook(); // xls 버전
Workbook wb = new SXSSFWorkbook(); //xlsx 버전
 
wb.createSheet(sheetName1);
wb.createSheet(sheetName2);
wb.createSheet();

Excel 파일 수정

엑셀 파일 내 셀의 내용을 변경하고 저장한다.

저장된 엑셀파일을 로드할 수 있으며 지정한 sheet에 row와 cell 객체를 생성하여 텍스트를 저장하고 수정할 수 있다.

Sample Source
// xls엑셀 파일 로드
Workbook wb = excelService.loadWorkbook(filename);
 
// xlsx엑셀 파일 로드
XSSFWorkbook wb = null;
wb = excelService.loadWorkbook(sb.toString(), wb);
 
log.debug("testModifyCellContents after loadWorkbook....");
 
Sheet sheet = wb.getSheetAt(0);
Font f2 = wb.createFont();
CellStyle cs = wb.createCellStyle();
cs = wb.createCellStyle();
 
cs.setFont(f2);
cs.setWrapText(true);
 
Row row = sheet.createRow(rownum);
row.setHeight((short) 0x349);
Cell cell = row.createCell(cellnum);
// xls 엑셀방식일 경우
cell.setCellType(Cell.CELL_TYPE_STRING);  
cell.setCellValue(new HSSFRichTextString(content));
 
// xlsx 엑셀방식일 경우
cell.setCellType(XSSFCell.CELL_TYPE_STRING);
cell.setCellValue(new XSSFRichTextString(content));
 
cell.setCellStyle(cs);
 
 
 
sheet.setColumnWidth(20, (int) ((50 * 8) / ((double) 1 / 20)));
 
FileOutputStream out = new FileOutputStream(filename);
wb.write(out);
out.close();

Excel 문서 속성 수정

엑셀 파일 문서의 속성(Header, Footer)을 수정한다.

Header 및 Footer 클래스로 엑셀문서의 Header와 Footer의 값과 속성을 설정할 수 있다.

Sample Source
// 엑셀 파일 로드
Workbook wb = excelService.loadWorkbook(filename); // xls 버전
Workbook wb = excelService.loadWorkbook(filename, new XSSFWorkbook()); // xlsx 버전
LOGGER.debug("testModifyCellContents after loadWorkbook....");
 
Sheet sheet = wb.createSheet("doc test sheet");
 
Row row = sheet.createRow(1);
Cell cell = row.createCell(1);
cell.setCellValue(new HSSFRichTextString("Header/Footer Test")); // xls 버전
cell.setCellValue(new XSSFRichTextString("Header/Footer Test")); // xlsx 버전
 
 
// Header
Header header = sheet.getHeader();
header.setCenter("Center Header");
header.setLeft("Left Header");
header.setRight(HSSFHeader.font("Stencil-Normal", "Italic") + HSSFHeader.fontSize((short) 16) + "Right Stencil-Normal Italic font and size 16"); // xls 버전
header.setRight(XSSFOddHeader.stripFields("&IRight Stencil-Normal Italic font and size 16")); // xlsx 버전
 
 
// Footer
// xls 버전
Footer footer = sheet.getFooter();
footer.setCenter(HSSFHeader.font("Fixedsys", "Normal") + HSSFHeader.fontSize((short) 12) + "- 1 -");
 
// xlsx 버전
Footer footer = (XSSFOddFooter) sheet.getFooter();
footer.setCenter(XSSFOddHeader.stripFields("Fixedsys"));
 
footer.setLeft("Left Footer");
footer.setRight("Right Footer");
 
// 엑셀 파일 저장
FileOutputStream out = new FileOutputStream(filename);
wb.write(out);
out.close();

셀 내용 추출

엑셀 파일을 읽어 특정 셀의 값을 얻어온다.

HSSFCell 클래스의 getRichStringCellValue, getNumericCellValue, getStringCellValue 등 다양한 type의 Cell 내용을 추출할 수 있다.

Sample Source
Workbook wbT = excelService.loadWorkbook(filename); // xls 버전
Workbook wbT = excelService.loadWorkbook(filename, new XSSFWorkbook()); // xlsx 버전
Sheet sheetT = wbT.getSheet("cell test sheet");
 
for (int i = 0; i < 100; i++) {
	Row row = sheet.createRow(i);
	for (int j = 0; j < 5; j++) {
		Cell cell = row.createCell(j);
		cell.setCellValue(new HSSFRichTextString("row " + i + ", cell " + j)); // xls 버전
		cell.setCellValue(new XSSFRichTextString("row " + i + ", cell " + j)); // xlsx 버전
		cell.setCellStyle(cs);
	}
}

셀 속성 추출

특정 셀의 속성(폰트, 사이즈 등)을 수정한다.

HSSFFont, HSSFCellStyle 등의 클래스를 이용하여 셀의 폰트, 사이즈 등의 셀 속성을 수정할 수 있다.

Sample Source
// 엑셀 파일 로드
Workbook wb = excelService.loadWorkbook(filename); // xls 버전
Workbook wb = excelService.loadWorkbook(filename, new XSSFWorkbook()); //xlsx 버전
 
Sheet sheet = wb.createSheet("cell test sheet2");
sheet.setColumnWidth((short) 3, (short) 200);	// column Width
 
CellStyle cs = wb.createCellStyle();
Font font = wb.createFont();
font.setFontHeight((short) 16);
font.setBoldweight((short) 3);
font.setFontName("fixedsys");
 
cs.setFont(font);
cs.setAlignment(CellStyle.ALIGN_RIGHT);	// cell 정렬
cs.setWrapText( true );
 
for (int i = 0; i < 100; i++) {
	HSSFRow row = sheet.createRow(i);
	row.setHeight((short)300); // row의 height 설정
 
	for (int j = 0; j < 5; j++) {
		HSSFCell cell = row.createCell((short) j);
		cell.setCellValue(new HSSFRichTextString("row " + i + ", cell " + j)); // xls 버전
		cell.setCellValue(new XSSFRichTextString("row " + i + ", cell " + j)); // xlsx 버전
		cell.setCellStyle( cs );
	}
}
 
// 엑셀 파일 저장
FileOutputStream out = new FileOutputStream(filename);
wb.write(out);
out.close();

공통 템플릿 사용

공통 템플릿을 사용하여 일관성을 유지한다. jXLS 오픈소스를 사용하여 작성된 템플릿에 지정된 값을 저장한다.

Sample Source
List<PersonHourVO> persons = new ArrayList<PersonHourVO>();
PersonHourVO person = new PersonHourVO();
person.setName("Yegor Kozlov");
person.setId("YK");
person.setMon(5.0);
person.setTue(8.0);
person.setWed(10.0);
person.setThu(5.0);
person.setFri(5.0);
person.setSat(7.0);
person.setSun(6.0);
 
persons.add(person); 
 
PersonHourVO person1 = new PersonHourVO();
person1.setName("Gisella Bronzetti");
person1.setId("GB");
person1.setMon(4.0);
person1.setTue(3.0);
person1.setWed(1.0);
person1.setThu(3.5);
person1.setSun(4.0);
 
persons.add(person1); 
 
Map<String, Object> beans = new HashMap<String, Object>();
beans.put("persons", persons);
XLSTransformer transformer = new XLSTransformer();
 
transformer.transformXLS(filename, beans, sbResult.toString());
Excel Template
<jx:forEach var="persons" items="${persons}">
${persons.name}	${persons.id}	${persons.mon}	${persons.tue}	${persons.wed}	${persons.thu}	${persons.fri}	${persons.sat}	${persons.sun}	$[C4+D4+E4+F4+G4+H4+I4]
</jx:forEach>
		Total Hrs:	$[SUM(C4)]	$[SUM(D4)]	$[SUM(E4)]	$[SUM(F4)]	$[SUM(G4)]	$[SUM(H4)]	$[SUM(I4)]	$[SUM(J4)]

excel-service-template

Template 적용결과

excel-service-template-result

Excel 다운로드

Configuration
<bean id="categoryExcelView" class="egovframework.rte.fdl.excel.download.CategoryExcelView" />
 
<!--
XSSF 형태의 다운로드의 경우 다음의 View를 등록하여 사용한다.
<bean id="CategoryPOIExcelView" class="egovframework.rte.fdl.excel.download.CategoryPOIExcelView" />
-->
 
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
	<property name="order" value="0" />
</bean>
Sample Source

Controller 클래스 작성 Map 사용

@RequestMapping("/sale/listExcelCategory.do")
public ModelAndView selectCategoryList() throws Exception {
 
	List<Map> lists = new ArrayList<Map>();
 
	Map<String, String> mapCategory = new HashMap<String, String>();
	mapCategory.put("id", "0000000001");
	mapCategory.put("name", "Sample Test");
	mapCategory.put("description", "This is initial test data.");
	mapCategory.put("useyn", "Y");
	mapCategory.put("reguser", "test");
 
	lists.add(mapCategory);
 
	mapCategory.put("id", "0000000002");
	mapCategory.put("name", "test Name");
	mapCategory.put("description", "test Deso1111");
	mapCategory.put("useyn", "Y");
	mapCategory.put("reguser", "test");
 
	lists.add(mapCategory);
 
	Map<String, Object> map = new HashMap<String, Object>();
	map.put("category", lists);
 
	return new ModelAndView("categoryExcelView", "categoryMap", map);
}

VO 사용

@RequestMapping("/sale/listExcelVOCategory.do")
public ModelAndView selectCategoryVOList() throws Exception {
 
	List<UsersVO> lists = new ArrayList<UsersVO>();
 
	UsersVO users = new UsersVO();
 
	//Map<String, String> mapCategory = new HashMap<String, String>();
	users.setId("0000000001");
	users.setName("Sample Test");
	users.setDescription("This is initial test data.");
	users.setUseYn("Y");
	users.setRegUser("test");
 
	lists.add(users);
 
	users.setId("0000000002");
	users.setName("test Name");
	users.setDescription("test Deso1111");
	users.setUseYn("Y");
	users.setRegUser("test");
 
	lists.add(users);
 
	Map<String, Object> map = new HashMap<String, Object>();
	map.put("category", lists);
 
	return new ModelAndView("categoryExcelView", "categoryMap", map);
}

View 클래스 작성(xls 버전)

public class CategoryExcelView extends AbstractExcelView {
 
	private static final Logger LOGGER  = LoggerFactory.getLogger(CategoryExcelView.class);
 
	@Override
	protected void buildExcelDocument(Map model, HSSFWorkbook wb, HttpServletRequest req, HttpServletResponse resp) throws Exception {
        HSSFCell cell = null;
 
        LOGGER.debug("### buildExcelDocument start !!!");
 
        HSSFSheet sheet = wb.createSheet("User List");
        sheet.setDefaultColumnWidth(12);
 
        // put text in first cell
        cell = getCell(sheet, 0, 0);
        setText(cell, "User List");
 
        // set header information
        setText(getCell(sheet, 2, 0), "id");
        setText(getCell(sheet, 2, 1), "name");
        setText(getCell(sheet, 2, 2), "description");
        setText(getCell(sheet, 2, 3), "use_yn");
        setText(getCell(sheet, 2, 4), "reg_user");
 
        LOGGER.debug("### buildExcelDocument cast");
 
 
        Map<String, Object> map= (Map<String, Object>) model.get("categoryMap");
        List<Object> categories = (List<Object>) map.get("category");
 
        boolean isVO = false;
 
        if (categories.size() > 0) {
        	Object obj = categories.get(0);
        	isVO = obj instanceof UsersVO;
        }
 
        for (int i = 0; i < categories.size(); i++) {
 
        	if (isVO) {	// VO
 
        		LOGGER.debug("### buildExcelDocument VO : {} started!!", i);
 
        		UsersVO category = (UsersVO) categories.get(i);
 
	            cell = getCell(sheet, 3 + i, 0);
	            setText(cell, category.getId());
 
	            cell = getCell(sheet, 3 + i, 1);
	            setText(cell, category.getName());
 
	            cell = getCell(sheet, 3 + i, 2);
	            setText(cell, category.getDescription());
 
	            cell = getCell(sheet, 3 + i, 3);
	            setText(cell, category.getUseYn());
 
	            cell = getCell(sheet, 3 + i, 4);
	            setText(cell, category.getRegUser());
 
	            LOGGER.debug("### buildExcelDocument VO : {} end!!", i);
 
        	 } else {	// Map
 
        		LOGGER.debug("### buildExcelDocument Map : {} started!!", i);
 
        		Map<String, String> category = (Map<String, String>) categories.get(i);
 
 	            cell = getCell(sheet, 3 + i, 0);
 	            setText(cell, category.get("id"));
 
 	            cell = getCell(sheet, 3 + i, 1);
 	            setText(cell, category.get("name"));
 
 	            cell = getCell(sheet, 3 + i, 2);
 	            setText(cell, category.get("description"));
 
 	            cell = getCell(sheet, 3 + i, 3);
 	            setText(cell, category.get("useyn"));
 
 	            cell = getCell(sheet, 3 + i, 4);
 	            setText(cell, category.get("reguser"));
 
 	            LOGGER.debug("### buildExcelDocument Map : {} end!!", i);
        	 }
        }
    }
}

View 클래스 작성(xlsx 버전)

public class CategoryPOIExcelView extends AbstractPOIExcelView {
 
	private static final Logger LOGGER  = LoggerFactory.getLogger(CategoryPOIExcelView.class);
 
	@Override
	protected void buildExcelDocument(Map model, XSSFWorkbook wb, HttpServletRequest req, HttpServletResponse resp) throws Exception {
        XSSFCell cell = null;
 
        LOGGER.debug("### buildExcelDocument start !!!");
 
        XSSFSheet sheet = wb.createSheet("User List");
        sheet.setDefaultColumnWidth(12);
 
        // put text in first cell
        cell = getCell(sheet, 0, 0);
        setText(cell, "User List");
 
        // set header information
        setText(getCell(sheet, 2, 0), "id");
        setText(getCell(sheet, 2, 1), "name");
        setText(getCell(sheet, 2, 2), "description");
        setText(getCell(sheet, 2, 3), "use_yn");
        setText(getCell(sheet, 2, 4), "reg_user");
 
        LOGGER.debug("### buildExcelDocument cast");
 
 
        Map<String, Object> map= (Map<String, Object>) model.get("categoryMap");
        List<Object> categories = (List<Object>) map.get("category");
 
        boolean isVO = false;
 
        if (categories.size() > 0) {
        	Object obj = categories.get(0);
        	isVO = obj instanceof UsersVO;
        }
 
        for (int i = 0; i < categories.size(); i++) {
 
        	if (isVO) {	// VO
 
        		LOGGER.debug("### buildExcelDocument VO : {} started!!", i);
 
        		UsersVO category = (UsersVO) categories.get(i);
 
	            cell = getCell(sheet, 3 + i, 0);
	            setText(cell, category.getId());
 
	            cell = getCell(sheet, 3 + i, 1);
	            setText(cell, category.getName());
 
	            cell = getCell(sheet, 3 + i, 2);
	            setText(cell, category.getDescription());
 
	            cell = getCell(sheet, 3 + i, 3);
	            setText(cell, category.getUseYn());
 
	            cell = getCell(sheet, 3 + i, 4);
	            setText(cell, category.getRegUser());
 
	            LOGGER.debug("### buildExcelDocument VO : {} end!!", i);
 
        	 } else {	// Map
 
        		LOGGER.debug("### buildExcelDocument Map : {} started!!", i);
 
        		Map<String, String> category = (Map<String, String>) categories.get(i);
 
 	            cell = getCell(sheet, 3 + i, 0);
 	            setText(cell, category.get("id"));
 
 	            cell = getCell(sheet, 3 + i, 1);
 	            setText(cell, category.get("name"));
 
 	            cell = getCell(sheet, 3 + i, 2);
 	            setText(cell, category.get("description"));
 
 	            cell = getCell(sheet, 3 + i, 3);
 	            setText(cell, category.get("useyn"));
 
 	            cell = getCell(sheet, 3 + i, 4);
 	            setText(cell, category.get("reguser"));
 
 	            LOGGER.debug("### buildExcelDocument Map : {} end!!", i);
        	 }
        }
    }
}

Excel 업로드

Configuration
<bean id="excelService"	class="egovframework.rte.fdl.excel.impl.EgovExcelServiceImpl">
	<property name="mapClass" value="egovframework.rte.fdl.excel.upload.EgovExcelTestMapping" />
	<property name="sqlSessionTemplate" ref="sqlSessionTemplate" />
</bean>
 
<bean id="excelBigService" class="egovframework.rte.fdl.excel.impl.EgovExcelServiceImpl">
	<property name="mapClass" value="egovframework.rte.fdl.excel.upload.EgovExcelTestMapping" />
	<property name="mapBeanName" value="mappingBean" />
	<property name="sqlMapClient" ref="sqlMapClient" />
</bean>
 
<bean id="mappingBean" class="egovframework.rte.fdl.excel.upload.EgovExcelBigTestMapping" />
  • class : egovframework.rte.fdl.excel.impl.EgovExcelServiceImpl
  • propertyPath : xml형식의 엑셀 형식정보 위치
  • mapClass : 개발자가 작성한 VO와 Query의 mapping을 위한 클래스
  • mapBeanName : Excel Cell과 VO를 mapping 구현 Bean name (mapClass보다 우선함)
  • sqlMapClient : ibatis의 sqlMapClient(ibatis 사용시 적용)
  • sqlSessionTemplate : mybatis의 sqlSessionTemplate(mybatis 사용시 적용)
Sample Source

VO 클래스 작성

public class EmpVO implements Serializable {
 
	private BigDecimal empNo;
	private String empName;
	private String job;
 
	public BigDecimal getEmpNo() {
		return empNo;
	}
 
  public void setEmpNo(BigDecimal empNo) {
  	this.empNo = empNo;
  }
 
	public String getEmpName() {
		return empName;
	}
 
	public void setEmpName(String empName) {
		this.empName = empName;
	}
 
	public String getJob() {
		return job;
	}
 
	public void setJob(String job) {
		this.job = job;
	}
}
  • 엑셀과 Query의 mapping을 위한 VO클래스

Mapping 클래스 작성

public class EgovExcelTestMapping extends EgovExcelMapping {
 
	private static final Logger LOGGER = LoggerFactory.getLogger(EgovExcelTestMapping.class);
 
	@Override
	public EmpVO mappingColumn(Row row) {
		Cell cell0 = row.getCell(0);
    	        Cell cell1 = row.getCell(1);
    	        Cell cell2 = row.getCell(2);
 
		EmpVO vo = new EmpVO();
 
		vo.setEmpNo(new BigDecimal(cell0.getNumericCellValue()));
		vo.setEmpName(EgovExcelUtil.getValue(cell1));
		vo.setJob(EgovExcelUtil.getValue(cell2));
 
		LOGGER.debug("########### vo is {}", vo.getEmpNo());
		LOGGER.debug("########### vo is {}", vo.getEmpName());
		LOGGER.debug("########### vo is {}", vo.getJob());
 
		return vo;
	}
}
  • 엑셀과 VO의 mapping을 위한 mapping클래스
  • EgovExcelMapping 클래스를 상속받아서 mappingColumn 메소드를 오버라이드하여 구현
  • HSSFCell 클래스에서 엑셀 값을 추출하여 Query를 실행시키기 위한 VO와 mapping
Query
<sqlMap namespace="EmpBatchInsert">
	<typeAlias alias="empVO" type="egovframework.rte.fdl.excel.vo.EmpVO" />
	<insert id="insertEmpUsingBatch" parameterClass="empVO">
		<![CDATA[
			insert into EMP (
				EMP_NO,
				EMP_NAME,
				JOB
			) values (
				#empNo#,
				#empName#,
				#job#
			)
		]]>
	</insert>
</sqlMap>

예제

2.38 - String Util Service

EgovStringUtil, EgovNumericUtil, EgovDateUtil, EgovObjectUtil 등의 서비스는 문자열, 숫자, 날짜, 객체 생성 등을 쉽게 다룰 수 있도록 다양한 기능을 제공한다. 이를 통해 패턴 매칭, 데이터 형식 변환, 숫자 계산, 날짜 계산, 객체 인스턴스화 등의 작업을 효율적으로 수행할 수 있다.

String Util Service

개요

시스템을 개발할 때 필요한 문자열 데이터를 다루기 위해 다양한 기능을 사용하도록 서비스한다. 문자열을 다루는 EgovStringUtil Service와 숫자를 다루는 EgovNumericUtil Service, 날짜형식을 다루는 EgovDateUtil Service 그리고 객체 생성 등의 EgovObjectUtil Service 4가지가 있다.

설명

1. EgovStringUtil Service

Pattern matching

String이 특정 Pattern(정규표현식)에 부합하는지 검사한다.

Sample Source
@Test
public void testPatternMatch() throws Exception {
  // pattern match 성공
  String str = "abc-def";
  pattern = "*-*";
  assertTrue(EgovStringUtil.isPatternMatching(str, pattern));
 
  // pattern match 실패
  str = "abc";
  assertTrue(!EgovStringUtil.isPatternMatching(str, pattern));
}

Formatting

다양한 타입의 데이터를 특정 String형식(Format)으로 변환한다.

Sample Source
@Test
public void testTypeConversion() throws Exception {
  // int => string
  assertEquals("1", EgovStringUtil.integer2string(1));
 
  // long => string
  assertEquals("1000000000", EgovStringUtil.long2string(1000000000));
 
  // float => string
  assertEquals("34.5", EgovStringUtil.float2string(34.5f));
 
  // double => string
  assertEquals("34.5", EgovStringUtil.double2string(34.5));
 
  // string => int
  assertEquals(1, EgovStringUtil.string2integer("1"));
  assertEquals(0, EgovStringUtil.string2integer(null, 0));
 
  // string => float
  assertEquals(Float.valueOf(34.5f), Float.valueOf(EgovStringUtil.string2float("34.5")));
  assertEquals(Float.valueOf(10.5f), Float.valueOf(EgovStringUtil.string2float(null, 10.5f)));
 
  // string => double
  assertEquals(Double.valueOf(34.5), Double.valueOf(EgovStringUtil.string2double("34.5")));
  assertEquals(Double.valueOf(34.5), Double.valueOf(EgovStringUtil.string2double(null, 34.5)));
 
  // string => long
  assertEquals(100000000, EgovStringUtil.string2long("100000000"));
  assertEquals(100000000, EgovStringUtil.string2long(null, 100000000));
}

Substring

전체 String 중 일부를 가져온다.

Sample Source
@Test
public void testToSubString() throws Exception {
  String source = "substring test";
 
  assertEquals("test", EgovStringUtil.toSubString(source, 10));
  assertEquals("string", EgovStringUtil.toSubString(source, 3, 9));
}

Trim

전체 String 중 앞뒤에 존재하는 공백 문자(white character)를 제거한다.

Sample Source
@Test
public void testStringTrim() throws Exception {
  String str = "  substring  ";
 
  assertEquals("substring"  , EgovStringUtil.trim(str));
  assertEquals("substring  ", EgovStringUtil.ltrim(str));
  assertEquals("  substring", EgovStringUtil.rtrim(str));
}

Concatenate

두 String을 붙여서 하나의 String을 생성한다.

Sample Source
@Test
public void testConcat() throws Exception {
  String str1 = "substring";
  String str2 = "test";
 
  assertEquals("substringtest", EgovStringUtil.concat(str1, str2));
}

Find

전체 String 중 특정 String Pattern이 있는지 찾는다.

Sample Source
@Test
public void testFindPattern() throws Exception {
  String pattern = "\\d{4}-\\d{1,2}-\\d{1,2}";
 
  // 일치하는 pattern 을 찾는다.
  Matcher matcher = Pattern.compile(pattern).matcher("2009-02-03");    	
  assertTrue(matcher.find());
 
  // 일치하는 pattern 을 찾는다.
  matcher = Pattern.compile(pattern).matcher("abcdef2009-02-03abcdef");
  assertTrue(matcher.find());
 
  // 일치하는 pattern 을 찾지 못한다.
  matcher = Pattern.compile(pattern).matcher("abcdef2009-02-A3abcdef");
  assertFalse(matcher.find());
}

2. EgovNumericUtil Service

숫자체크, 더하기, 빼기, 곱하기, 나누기, 올림, 내림 기능

숫자체크 기능

주어진 문자열이 숫자형식인지 검사한다.

Sample Source
@Test
public void testIsNumber() throws Exception {
  assertFalse(EgovNumericUtil.isNumber("abc"));
  assertFalse(EgovNumericUtil.isNumber("!@"));
  assertFalse(EgovNumericUtil.isNumber("ab-123"));
  assertTrue(EgovNumericUtil.isNumber("-123"));
  assertTrue(EgovNumericUtil.isNumber("1234"));
}

더하기 기능

두 문자열 값의 덧셈을 실행한다.

Sample Source
@Test
public void testPlus() throws Exception {
  assertEquals("400",      EgovNumericUtil.plus("151", "249"));
  assertEquals("400.0000", EgovNumericUtil.plus("151.7531", "248.2469"));
  assertEquals("400.000",  EgovNumericUtil.plus("151.7531", "248.2469", 3));
  assertEquals("399.9654", EgovNumericUtil.plus("151.7531", "248.2123"));
  assertEquals("399.966",  EgovNumericUtil.plus("151.7531", "248.2123", 3, EgovNumericUtil.ROUND_UP));
  assertEquals("399.965",  EgovNumericUtil.plus("151.7531", "248.2123", 3, EgovNumericUtil.ROUND_DOWN));
  assertEquals("399.97",   EgovNumericUtil.plus("151.7531", "248.2123", 2, EgovNumericUtil.ROUND_HALF_UP));
}

빼기 기능

두 문자열 값의 뺄셈을 실행한다.

Sample Source
@Test
public void testMinus() throws Exception {
  assertEquals("89",       EgovNumericUtil.minus("240", "151"));
  assertEquals("96.4938",  EgovNumericUtil.minus("248.2469", "151.7531"));
  assertEquals("96.49380", EgovNumericUtil.minus("248.2469", "151.7531", 5));
  assertEquals("96.4592",  EgovNumericUtil.minus("248.2123", "151.7531"));
  assertEquals("96.460",   EgovNumericUtil.minus("248.2123", "151.7531", 3, EgovNumericUtil.ROUND_UP));
  assertEquals("96.459",   EgovNumericUtil.minus("248.2123", "151.7531", 3, EgovNumericUtil.ROUND_DOWN));
  assertEquals("96.46",    EgovNumericUtil.minus("248.2123", "151.7531", 2, EgovNumericUtil.ROUND_HALF_UP));
}

곱하기 기능

두 문자열 값의 곱셈을 실행한다.

Sample Source
@Test
public void testMultiply() throws Exception {
  assertEquals("180",       EgovNumericUtil.multiply("15", "12"));
  assertEquals("189.6135",  EgovNumericUtil.multiply("15.23", "12.45"));
  assertEquals("189.61350", EgovNumericUtil.multiply("15.23", "12.45", 5));
  assertEquals("189.614",   EgovNumericUtil.multiply("15.23", "12.45", 3, EgovNumericUtil.ROUND_UP));
  assertEquals("189.613",   EgovNumericUtil.multiply("15.23", "12.45", 3, EgovNumericUtil.ROUND_DOWN));
  assertEquals("189.61",    EgovNumericUtil.multiply("15.23", "12.45", 2, EgovNumericUtil.ROUND_HALF_UP));
}

나누기 기능

두 문자열 값의 나눗셈을 실행한다.

Sample Source
@Test
public void testDivide() throws Exception {
  assertEquals("1.25",  EgovNumericUtil.divide("15", "12"));
 
  Class<Exception> exceptionClass = null;
  try {
  	assertEquals("1.2232931726907630522088353413655", EgovNumericUtil.divide("15.23", "12.45"));
  } catch (Exception e) {
  	log.error("### Exception : " + e.toString());
  	exceptionClass = (Class<Exception>) e.getClass();
  } finally {
  	assertEquals(ArithmeticException.class, exceptionClass);
  }
 
  assertEquals("1.22",  EgovNumericUtil.divide("15.23", "12.45", 5));
  assertEquals("1.224", EgovNumericUtil.divide("15.23", "12.45", 3, EgovNumericUtil.ROUND_UP));
  assertEquals("1.223", EgovNumericUtil.divide("15.23", "12.45", 3, EgovNumericUtil.ROUND_DOWN));
  assertEquals("1.22",  EgovNumericUtil.divide("15.23", "12.45", 2, EgovNumericUtil.ROUND_HALF_UP));
}

올림, 내림 기능

주어진 값의 반올림, 올림, 내림을 실행한다.

Sample Source
@Test
public void testScale() throws Exception {
  assertEquals("151.754", EgovNumericUtil.setScale("151.7531", 3, EgovNumericUtil.ROUND_UP));
  assertEquals("151.753", EgovNumericUtil.setScale("151.7531", 3, EgovNumericUtil.ROUND_DOWN));
  assertEquals("151.753", EgovNumericUtil.setScale("151.7531", 3, EgovNumericUtil.ROUND_HALF_UP));
}

3. EgovDateUtil Service

날짜계산, 현재일자 조회, 요일, 날짜형식체크 기능

날짜계산 기능

주어진 날짜에 해당 년,월 또는 일자를 더하여 계산된 일자를 조회한다.

Sample Source
@Test
public void testCalcDate() throws Exception {
  assertEquals("20100114",  EgovDateUtil.getCalcDateAsString ("2009", "3", "20", 300, "day"));
  assertEquals("2010",      EgovDateUtil.getCalcYearAsString ("2009", "3", "20", 300, "day"));
  assertEquals("01",        EgovDateUtil.getCalcMonthAsString("2009", "3", "20", 300, "day"));
  assertEquals("14",        EgovDateUtil.getCalcDayAsString  ("2009", "3", "20", 300, "day"));
 
  assertEquals(2010,        EgovDateUtil.getCalcYearAsInt ("2009", "3", "20", 300, "day"));
  assertEquals(1,           EgovDateUtil.getCalcMonthAsInt("2009", "3", "20", 300, "day"));
  assertEquals(14,          EgovDateUtil.getCalcDayAsInt  ("2009", "3", "20", 300, "day"));
}

시작일자와 종료일자 및 두 시간 사이의 일자/밀리초 수를 계산한다.

Sample Source
@Test
public void testDayCount() throws Exception {
  assertEquals(90,  EgovDateUtil.getDayCount("20090101", "20090401"));
  assertEquals(90,  EgovDateUtil.getDayCountWithFormatter("20090101", "20090401", "yyyyMMdd"));
  assertEquals(182, EgovDateUtil.getDayCountWithFormatter("2008/12/01", "2009/06/01", "yyyy/MM/dd"));
}
 
@Test
public void testTimeCount() throws Exception {
  assertEquals(86400000, EgovDateUtil.getTimeCount("20090401",       "20090402"));
  assertEquals(60000,    EgovDateUtil.getTimeCount("20090301000000", "20090301000100"));
  assertEquals(3600000,  EgovDateUtil.getTimeCount("20090301000000", "20090301010000"));
}

현재일자 조회 기능

현재 일자를 조회한다.

Sample Source
@Test
public void testCurrentDate() throws Exception {
  assertEquals(Calendar.getInstance().get(Calendar.YEAR),         EgovDateUtil.getCurrentYearAsInt());
  assertEquals(Calendar.getInstance().get(Calendar.MONTH) + 1,    EgovDateUtil.getCurrentMonthAsInt());
  assertEquals(Calendar.getInstance().get(Calendar.DAY_OF_MONTH), EgovDateUtil.getCurrentDayAsInt());
  assertEquals(Calendar.getInstance().get(Calendar.HOUR_OF_DAY),  EgovDateUtil.getCurrentHourAsInt());
  assertEquals(Calendar.getInstance().get(Calendar.MINUTE),       EgovDateUtil.getCurrentMinuteAsInt());
}

요일 기능

입력 일자의 해당 요일을 조회한다.

Sample Source
@Test
public void testGetDayOfWeek() throws Exception {
  assertEquals("일", EgovDateUtil.getDayOfWeekAsString("2009", "03", "22"));
  assertEquals("월", EgovDateUtil.getDayOfWeekAsString("2009", "03", "23"));
  assertEquals("화", EgovDateUtil.getDayOfWeekAsString("2009", "03", "24"));
  assertEquals("수", EgovDateUtil.getDayOfWeekAsString("2009", "03", "25"));
  assertEquals("목", EgovDateUtil.getDayOfWeekAsString("2009", "03", "26"));
  assertEquals("금", EgovDateUtil.getDayOfWeekAsString("2009", "03", "27"));
  assertEquals("토", EgovDateUtil.getDayOfWeekAsString("2009", "03", "28"));
}

두 일자 사이에 해당 요일의 수를 조회한다.

Sample Source
@Test
public void testGetDayOfWeek() throws Exception {
  assertEquals(5,  EgovDateUtil.getDayOfWeekCount("20090301", "20090331", "일요일"));
  assertEquals(4,  EgovDateUtil.getDayOfWeekCount("20090301", "20090331", "토요일"));
  assertEquals(22, EgovDateUtil.getDayOfWeekCount("20090101", "20090531", "일"));
  assertEquals(52, EgovDateUtil.getDayOfWeekCount("20090101", "20091231", "일"));
  assertEquals(52, EgovDateUtil.getDayOfWeekCount("20090101", "20091231", "금"));
  assertEquals(52, EgovDateUtil.getDayOfWeekCount("20090101", "20091231", "토"));
}

날짜 형식 체크 기능

해당 날짜의 형식이 적합성을 조회한다.

Sample Source
@Test
public void testDateFormatCheck() throws Exception {
  // 형식이 틀린경우 ParseException 발생
  Class<Exception> exceptionClass = null;
 
  try {
  	dateFormatCheck = EgovDateUtil.dateFormatCheck("20090300");
  } catch (Exception e) {
  	exceptionClass = (Class<Exception>) e.getClass();
  } finally {
  	assertEquals(ParseException.class, exceptionClass);
  }
}

4. EgovObjectUtil Service

클래스명으로 객체를 생성하며 객체는 파라미터가 없는 기본 생성자 또는 파라미터가 존재하는 생성자 등 다양한 형태로 객체를 인스턴스화 할 수 있다.

Instantiate 기능

파라미터가 없는 기본 생성자로 객체를 인스턴스화 한다.

Sample Source
@Test
public void testInstantiate() throws Exception {
  String className = "java.lang.String";
  Object object = EgovObjectUtil.instantiate(className);
 
  String string = (String) object;
  string = "eGovFramework";
  assertEquals("Framework", string.substring(4));
}

Instantiate 기능 - 생성자의 파라미터 포함

파라미터가 존재하는 형태의 생성자로 객체를 인스턴스화 한다.

Sample Source
@Test
public void testInstantiateParamConstructor() throws Exception {
  String className = "java.lang.StringBuffer";
  String[] types = new String[]{"java.lang.String"};
  Object[] values = new Object[]{"전자정부 공통서비스"};
 
  StringBuffer sb = (StringBuffer)EgovObjectUtil.instantiate(className, types, values);
  sb.append(" 및 개발프레임워크 구축 사업");		
  assertEquals("전자정부 공통서비스 및 개발프레임워크 구축 사업", sb.toString());
}

참고자료

3 - 화면처리

화면처리 서비스그룹은 업무처리 서비스와 사용자간의 인터페이스를 담당하는 서비스로 사용자 화면 구성 및 사용자 입력 정보 검증 등의 기능을 지원한다.

화면처리

화면처리 서비스그룹은 업무처리 서비스와 사용자간의 인터페이스를 담당하는 서비스로 사용자 화면 구성 및 사용자 입력 정보 검증 등의 기능을 지원한다.

3.1 - MVC 패턴의 구조와 장점

MVC 패턴은 애플리케이션의 기능을 Model, View, Controller로 분리하여 UI 코드와 비즈니스 로직의 종속성을 줄이고, 재사용성과 변경 용이성을 높인다. 표준프레임워크에서 “MVC 서비스"는 이 패턴을 활용한 Web MVC Framework를 의미한다.

Web Servlet

개요

MVC(Model-View-Controller) 패턴

MVC(Model-View-Controller) 패턴은 코드를 기능(역할)에 따라 Model, View, Controller 3가지 요소로 분리한다.

  • Model : 애플리케이션의 데이터와 비즈니스 로직을 담는 객체이다.
  • View : Model의 정보를 사용자에게 표시한다. 하나의 Model을 다양한 View에서 사용할 수 있다.
  • Controller : Model과 View의 중계 역할을 한다. 사용자의 요청을 받아 Model에 변경된 상태를 반영하고, 응답을 위한 View를 선택한다.

MVC 패턴은 UI 코드와 비즈니스 코드를 분리함으로써 종속성을 줄이고, 재사용성을 높이고, 보다 쉬운 변경이 가능하도록 한다.

MVC 패턴이 Web Framework에만 사용되는 단어는 아니지만, 표준프레임워크에서 “MVC 서비스”란 MVC 패턴을 활용한 Web MVC Framework를 의미한다.

설명

오픈소스 Web MVC Framework에는 Spring MVC, Struts, Webwork, JSF 등이 있으며, 각각의 장점을 가지고 사용되고 있다.
기능상에서 큰 차이는 없으나, 아래와 같은 장점을 고려 표준프레임워크에서는 Spring Web MVC를 MVC 서비스의 기반 오픈 소스로 채택하였다.

  • Framework 내의 특정 클래스를 상속하거나, 참조, 구현해야 하는 등의 제약사항이 비교적 적다.
    Controller(Spring 2.5 @MVC)나 Form 클래스 등이 좀 더 POJO-style에 가까워 비즈니스 로직에 집중된 코드를 작성할 수 있다.
  • IOC Container가 Spring이라면 (간단한 설정으로 Struts나 Webwork 같은 Web Framework을 사용할 수 있겠지만) 연계를 위한 추가 설정 없이 Spring MVC를 사용할 수 있다.
    표준프레임워크의 IOC Container는 Spring이다.
  • 오픈소스 프로젝트가 활성화(꾸준한 기능 추가, 빠른 bug fix와 Q&A) 되어 있으며 로드맵이 신뢰할만하다.
  • 국내 커뮤니티 활성화 정도, 관련 참고 문서나 도서를 쉽게 구할 수 있다.

Spring MVC

Spring MVC에 대한 설명은 아래 상세 페이지를 참고하라.

예제 실행

참고 자료

서블릿 개발과 동작과정.png

서블릿 개발과 동작과정

3.2 - Spring MVC Architecture

Spring MVC는 DispatcherServlet, HandlerMapping, Controller, ViewResolver 등 각 컴포넌트의 역할이 명확히 분리되어 있으며, 다양한 인터페이스와 구현 클래스를 제공해 유연한 선택이 가능하다. POJO 스타일의 클래스 작성으로 비즈니스 로직에 집중할 수 있으며, 웹 요청 파라미터와 커맨드 클래스 간 데이터 매핑, 데이터 검증, 오류 처리 기능을 지원한다. 또한 JSP 폼 구성을 위한 태그 라이브러리도 제공한다.

Spring MVC Architecture

개요

Spring Framework은 간단한 설정만으로 Struts나 Webwork같은 Web Framework을 사용할 수 있지만, 자체적으로 MVC Web Framework을 가지고 있다. Spring MVC는 기본요소인 Model, View, Controller 외에도, 아래와 같은 특성을 가지고 있다.

  • DispatcherServlet, HandlerMapping, Controller, Interceptor, ViewResolver, View등 각 컴포넌트들의 역할이 명확하게 분리되어 있다.
  • HandlerMapping, Controller, View등 컴포넌트들에 다양한 인터페이스 및 구현 클래스를 제공함으로써 경우에 따라 선택하여 사용할 수 있다.
  • Controller(@MVC)나 폼 클래스(커맨드 클래스) 작성시에 특정 클래스를 상속받거나 참조할 필요 없이 POJO 나 POJO-style의 클래스를 작성함으로써 비지니스 로직에 집중한 코드를 작성할 수 있다.
  • 웹요청 파라미터와 커맨드 클래스간에 데이터 매핑 기능을 제공한다.
  • 데이터 검증을 할 수 있는, Validator와 Error 처리 기능을 제공한다.
  • JSP Form을 쉽게 구성하도록 Tag를 제공한다.

설명

Spring MVC(Model-View-Controller)의 핵심 Component는 아래와 같다.

Component개요
DispatcherServletSpring MVC Framework의 Front Controller, 웹요청과 응답의 Life Cycle을 주관한다.
HandlerMapping웹요청시 해당 URL을 어떤 Controller가 처리할지 결정한다.
Controller비지니스 로직을 수행하고 결과 데이터를 ModelAndView에 반영한다.
ModelAndViewController가 수행 결과를 반영하는 Model 데이터 객체와 이동할 페이지 정보(또는 View객체)로 이루어져 있다.
ViewResolver어떤 View를 선택할지 결정한다.
View결과 데이터인 Model 객체를 display한다.

이들 컴포넌트간의 관계와 흐름을 그림으로 나타내면 아래와 같다.

web-servlet–spring-mvc-architecture

  1. Client의 요청이 들어오면 DispatchServlet이 가장 먼저 요청을 받는다.
  2. HandlerMapping이 요청에 해당하는 Controller를 return한다.
  3. Controller는 비지니스 로직을 수행(호출)하고 결과 데이터를 ModelAndView에 반영하여 return한다.
  4. ViewResolver는 view name을 받아 해당하는 View 객체를 return한다.
  5. View는 Model 객체를 받아 rendering한다.

이 가이드문서는 Spring 2.5.6 버젼을 기준으로 작성되었다.

참고자료

  • The Spring Framework - Reference Documentation 2.5.6
  • Spring Framework API Documentation 2.5.6

3.3 - DispatcherServlet

DispatcherServlet은 Spring MVC의 유일한 Front Controller로, 모든 웹 요청을 처리하고 결과 데이터를 클라이언트에 응답하는 핵심 요소이다. 웹 요청의 전체 라이프사이클을 주관하며 Controller로의 진입점 역할을 한다.

DispatcherServlet

개요

Spring MVC Framework의 유일한 Front Controller인 DispatcherServlet은 Spring MVC의 핵심 요소이다. DispatcherServlet은 Controller로 향하는 모든 웹요청의 진입점이며, 웹요청을 처리하며, 결과 데이터를 Client에게 응답 한다. DispatcherServlet은 Spring MVC의 웹요청 Life Cycle을 주관한다 할 수 있다.

설명

DispatcherServlet에서의 웹요청 흐름

Client의 웹요청시에 DispatcherServlet에서 이루어지는 처리 흐름은 아래와 같다. 좀더 자세한 처리 흐름을 알고 싶다면 디버깅모드로 과정을 추적해 보는 것을 권장한다.

DispatcherServlet flow

  1. doService 메소드에서부터 웹요청의 처리가 시작된다. DispatcherServlet에서 사용되는 몇몇 정보를 request 객체에 담는 작업을 한 후 doDispatch 메소드를 호출한다.
  2. 아래 3번~13번 작업이 doDispatch 메소드안에 있다. Controller, View 등의 컴포넌트들을 이용한 실제적인 웹요청처리가 이루어 진다.
  3. getHandler 메소드는 RequestMapping 객체를 이용해서 요청에 해당하는 Controller를 얻게 된다.
  4. 요청에 해당하는 Handler를 찾았다면 Handler를 HandlerExecutionChain 객체에 담아 리턴하는데, 이때 HandlerExecutionChain는 요청에 해당하는 interceptor들이 있다면 함께 담아 리턴한다.
  5. 실행될 interceptor들이 있다면 interceptor의 preHandle 메소드를 차례로 실행한다.
  6. Controller의 인스턴스는 HandlerExecutionChain의 getHandler 메소드를 이용해서 얻는다.
  7. HandlerMapping과 마찬가지로 여러개의 HanlderAdaptor를 설정할 수 있는데, getHandlerAdaptor 메소드는 Controller에 적절한 HanlderAdaptor 하나를 리턴한다.
  8. 선택된 HanlderAdaptor의 handle 메소드가 실행되는데, 실제 실행은 파라미터로 넘겨 받은 Controller를 실행한다.
  9. 계층형 Controller인 경우는 handleRequest 메소드가 실행된다. @Controller인 경우는 HanlderAdaptor(AnnotationMethodHandlerAdapter)가 HandlerMethodInvoker를 이용해 실행할 Controller의 메소드를 invoke()한다.
  10. interceptor의 postHandle 메소드가 실행된다.
  11. resolveViewName 메소드는 논리적 뷰 이름을 가지고 해당 View 객체를 반환한다.
  12. Model 객체의 데이터를 보여주기 위해 해당 View 객체의 render 메소드가 수행된다.

web.xml에 DispatcherServlet 설정하기

Spring MVC Framework을 사용하기 위해서는 web.xml에 DispatcherServlet을 설정하고, DispatcherServlet이 WebApplicationContext를 생성할수 있도록 빈(Bean) 정보가 있는 파일들도 설정해주어야 한다.

기본 설정

<web-app>
        <!-- easycompnay라는 웹어플리케이션의 웹요청을 DispatcherServlet이 처리한다.-->
        <servlet>
		<servlet-name>easycompany</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	</servlet>
</web-app>

servlet-name은 DispatcherServlet이 기본(default)으로 참조할 빈 설정 파일 이름의 prefix가 되는데, (servlet-name)-servlet.xml 같은 형태이다. 위 예제와 같이 web.xml을 작성했다면 DispatcherServlet은 기본으로 /WEB-INF/easycompany-servlet.xml을 찾게 된다.

contextConfigLocation을 이용한 설정

빈 설정 파일을 하나 이상을 사용하거나, 파일 이름과 경로를 직접 지정해주고 싶다면 contextConfigLocation 라는 초기화 파라미터 값에 빈 설정 파일 경로를 설정해준다.

...
<servlet>
    <servlet-name>easycompany</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
                        /WEB-INF/config/easycompany-web.xml
        </param-value>
    </init-param>
</servlet>

ContextLoaderListener를 이용한 설정

일반적으로 빈 설정 파일은 하나의 파일만 사용되기 보다는 persistance, service, web등 layer 단위로 나뉘게 된다. 또한, 같은 persistance, service layer의 빈을 2개 이상의 DispatcherServlet이 공통으로 사용할 경우도 있다. 이럴때는 공통빈(persistance, service)설정 정보는 ApplicationContext에, web layer의 빈들은 WebApplicationContext에 저장하는 아래와 같은 방법을 추천한다. 공통빈 설정 파일은 서블릿 리스너로 등록된 org.springframework.web.context.ContextLoaderListener로 로딩해서 ApplicationContext을 만들고, web layer의 빈설정 파일은 DispatcherServlet이 로딩해서 WebApplicationContext을 만든다.

....
    <!-- ApplicationContext 빈 설정 파일-->
    <context-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>
                        <!--빈 설정 파일들간에 구분은 줄바꿈(\n),컴마(,),세미콜론(;)등으로 한다.-->
                        /WEB-INF/config/easycompany-service.xml,/WEB-INF/config/easycompany-dao.xml 
                </param-value>
	</context-param>
	
    <!-- 웹 어플리케이션이 시작되는 시점에 ApplicationContext을 로딩하며, 로딩된 빈정보는 모든 WebApplicationContext들이 참조할 수 있다.-->
    <listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

	<servlet>
		<servlet-name>employee</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>
				/WEB-INF/config/easycompany-service.xml
			</param-value>
		</init-param>
	</servlet>
        
    <servlet>
		<servlet-name>webservice</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>
				/WEB-INF/config/easycompany-webservice.xml
			</param-value>
		</init-param>
	</servlet>
....

이 ApplicationContext의 빈 정보는 모든 WebApplicationContext들이 참조할 수 있다. 예를 들어, DispatcherServlet은 2개 사용하지만 같은 Service, DAO를 사용하는 web.xml을 아래와 같이 작성했다면, easycompany-servlet.xml에 정의된 빈정보는 easycompany-webservice.xml가 참조할 수 없지만, easycompany-service.xml, easycompany-dao.xml에 설정된 빈 정보는 easycompany-servlet.xml, easycompany-webservice.xml 둘 다 참조한다. ApplicationContext과 WebApplicationContext과의 관계를 그림으로 나타내면 아래와 같다.

web-servlet-dispatcherservlet-relation

참고자료

  • The Spring Framework - Reference Documentation 2.5.6
  • Spring Framework API Documentation 2.5.6

3.4 - HandlerMapping

DispatcherServlet은 요청을 처리할 Controller를 HandlerMapping을 통해 매핑하며, Spring MVC는 여러 종류의 HandlerMapping 구현 클래스를 제공한다. Spring 3.1 이후 버전부터 기본 HandlerMapping은 RequestMappingHandlerMapping이며, 그 이전 버전에서는 DefaultAnnotationHandlerMapping이 기본으로 사용되었다.

HandlerMapping

개요

DispatcherServlet에 Client로부터 Http Request가 들어 오면 HandlerMapping은 요청처리를 담당할 Controller를 mapping한다. Spring MVC는 interface인 HandlerMapping의 구현 클래스도 가지고 있는데, 용도에 따라 여러 개의 HandlerMapping을 사용하는 것도 가능하다. 빈 정의 파일에 HandlerMapping에 대한 정의가 없다면 Spring MVC는 기본(default) HandlerMapping을 사용한다.

기본 HandlerMapping은 BeanNameUrlHandlerMapping이며, jdk1.5 이상의 실행환경일 때, Spring 3.1이후 버전이면(egov 3.0부터) RequestMappingHandlerMapping가 기본 HandlerMapping이며, Spring 3.1이전 버전이면(egov 3.0이전 버전) DefaultAnnotationHandlerMapping가 기본 HandlerMapping이다. (DefaultAnnotationHAndlerMapping은 3.1부터 deprecated되고 RequestMappingHandlerMapping으로 대체됨)

설명

BeanNameUrlHandlerMapping, SimpleUrlHandlerMapping 등 주요 HandlerMapping 구현 클래스는 상위 추상 클래스인 AbstractHandlerMapping과 AbstractUrlHandlerMapping을 확장하기 때문에 이 추상클래스들의 프로퍼티를 사용한다. 주요 프로퍼티는 아래와 같다.

  • defaultHandler
    • 요청에 해당하는 Controller가 없을 경우, defaultHandler에 등록된 Controller를 반환한다.
  • alwaysUseFullPath
    • URL과 Controller 매핑시에 URL full path를 사용할지 여부를 나타낸다.
    • 예를 들어, servlet-mapping이 /easycompany/* 이고, alwaysUseFullPath가 true이면 /easycompany/employeeList.do, alwaysUseFullPath가 false이면 /employeeList.do 이다.
  • interceptors
    • Controller가 요청을 처리하기 전,후로 특정한 로직을 수행되기 원할때 interpceptor를 등록한다. 복수개의 interpceptor를 등록할 수 있다. interceptor에 대한 자세한 설명은 이곳을 참고하라.
  • order
    • 여러개의 HandlerMapping 사용시에 우선순위.
...
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" p:order="2"/>
<bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping" p:order="3">
...
  • pathMatcher : 사용자 요청 URL path와 설정정보의 URL path를 매칭할때, 특정 스타일의 매칭을 지원하는 PathMatcher를 등록할수 있다. 기본값은 Ant-style의 패턴매칭을 제공하는 AntPathMatcher이다.

Spring MVC(3.1이후버전) 제공하는 주요 HandlerMapping 구현 클래스는 아래와 같다.

  • BeanNameUrlHandlerMapping
  • RequestMappingHandlerMapping(DefaultAnnotationHandlerMapping이 deprecated되면서 대체됨)
  • ControllerClassNameHandlerMapping
  • SimpleUrlHandlerMapping

Spring 3 버전 이전에는 @MVC에서 DefaultAnnotationHandlerMapping은 URL 단위로 interceptor를 적용할 수 없기에 전자정부프레임워크에서 아래와 같은 HandlerMapping 구현 클래스를 추가했다.

  • SimpleUrlAnnotationHandlerMapping

그러나 Spring 3버전부터 mvc:interceptors element에서 url별로 interceptor를 적용할 수 있도록 추가하여 SimpleUrlAnnotationHandlerMapping은 deprecated되었다.

BeanNameUrlHandlerMapping

BeanNameUrlHandlerMapping은 빈정의 태그에서 name attribute에 선언된 URL과 class attribute에 정의된 Controller를 매핑하는 방식으로 동작한다. 예를 들어, 아래와 같이 정의되어 있다면,

<beans ...>
...
   <!--HandlerMapping이 BeanNameUrlHandlerMapping 밖에 없다면 BeanNameUrlHandlerMapping에 대한 별도의 빈정의는 필요 없다.-->
   <!--<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>--> 
   <bean name="/insertEmployee.do" class="com.easycompany.controller.InsertEmployeeController">
       ...
   </bean>
...
</beans>

Client에서 URL ~~/insertEmployee.do 요청이 들어오면 InsertEmployeeController 클래스가 요청 처리를 담당한다.

앞 개요에서 언급했듯이 WAC(WebgApplicationContext)에 HandlerMapping 빈정의가 없다면 BeanNameUrlHandlerMapping이 (별도의 빈 정의 없이) 사용된다. 하지만, SimpleUrlHandlerMapping 같은 다른 HandlerMapping과 같이 써야 한다면, BeanNameUrlHandlerMapping도 빈정의가 되어야 한다.

<beans ...>
...
   <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
      <property name="mappings">
       ....
      </property>
   </bean>
 
   <bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>
   <bean name="/insertEmployee.do" class="com.easycompany.controller.InsertEmployeeController">
       ....
   </bean>
...
</beans>

RequestMappingHandlerMapping ( @deprecated DefaultAnnotationHandlerMapping과 거의 동일 )

@MVC 개발을 하려면 RequestMappingHandlerMapping을(Spring 3.1이전버전은 DefaultAnnotationHandlerMapping) 사용해야 한다. 단, jdk 1.5 이상의 개발환경이어야 한다. RequestMappingHandlerMapping 사용 방법은 세가지가 있다.

  • 선언하지 않는 방법
  • <mvc:annotation-driven/>을 선언하는 방법
  • RequestMappingHandlerMapping을 직접 선언하는 방법

선언하지 않는 방법

RequestMappingHandlerMapping은 기본 HandlerMapping이므로 지정하지 않아도 사용가능하다. 아래와 같이 컴포넌트 스캔할 패키지를 지정해 주면,

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
				http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
 
       <context:component-scan base-package="org.mycode.controller" />
 
</beans>

패키지 org.mycode.controller 아래의 @Controller중에 @RequestMapping에 선언된 URL과 해당 @Controller 클래스의 메소드와 매핑한다.

mvc:annotation-driven/을 선언하는 방법

@MVC사용 시 필요한 빈들을 등록해주는 <mvc:annotation-driven/>을 설정하면 내부에서 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping ,org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter 이 구성된다.

  • 기존 Spring 3.0버전에서 <mvc:annotation-driven/>는 DefaultAnnotationHandlerMapping, AnnotationMethodHandlerAdapter를 구성해주었다.

<mvc:annotation-driven>은 다음과 같이 사용한다.

<?xml version="1.0" encoding="UTF-8"?>
<beans ... 생략>
  <mvc:annotation-driven/>
<!-- 생략 -->
</beans>
  • 여기서 주의할 점은, CommandMap을 사용할 경우 mvc:annotation-driven을 선언하면 안된다. CommandMap을 사용할 경우 EogvRequestMappingHandlerAdapter와 RequestMappingHandlerMapping을 직접 선언해주어야한다.

RequestMappingHandlerMapping을 직접 선언하는 방법

다른 HandlerMapping과 함께 사용할 때 선언한다.

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
<!--생략-->

Controller코드 예제

위와 같이 RequestMappingHandlerMapping을 설정하였을 때

Controller에서의 간단한 예제를 보면,

package org.mycode.controller;
....
@Controller
public class HelloController {
 
      @RequestMapping(value="/hello.do")
      public String hellomethod(){
           ......   
      }
}

/hello.do로 URL 요청이 들어 오면 HelloController의 메소드 hellomethod가 실행된다.

ControllerClassNameHandlerMapping

ControllerClassNameHandlerMapping은 빈정의된 Controller의 클래스 이름중 suffix인 Controller를 제거한 나머지 이름의 소문자로 url mapping한다.

<beans ..>
   ...
   <bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>
 
   <bean class="com.easycompany.controller.hierarchy.EmployeeListController"/>
 
   <bean class="com.easycompany.controller.hierarchy.InsertEmployeeController"/>
   ...
</beans>

빈 정의가 위와 같다면, EmployeeListController ↔ /employeelist*, InsertEmployeeController ↔ /insertemployee* 과 같이 url mapping이 이루어 진다. ControllerClassNameHandlerMapping에 프로퍼티 값으로 caseSensitive나 pathPrefix, basePackage등을 설정할 수 있는데,

  • caseSensitive
    • Controller 이름으로 URL 경로 mapping시에 대문자 사용여부. (ex. /insertemployee* 가 아니라 /easycompany/insertEmployee*로 사용하기 원할때).
  • pathPrefix
    • URL 경로에 기본적인 prefix 값. 기본값은 false이다.
  • basePackage
    • URL mapping에 사용되는 Controller의 기본 패키지 이름이다. 사용되는 Controller의 패키지명에 기본 패키지에 추가되는 subpackage가 있다면 해당 subpackage 이름이 URL 경로에 추가된다.
<beans ..>
   ...
   <bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping">
      <property name="pathPrefix" value="/easycompany"/>
      <property name="caseSensitive" value="true"/> 
      <property name="basePackage" value="com.easycompany.controller"/>
   </bean>
 
   <bean class="com.easycompany.controller.hierarchy.EmployeeListController"/>
 
   <bean class="com.easycompany.controller.hierarchy.InsertEmployeeController"/>
   ...
</beans>

하면, EmployeeListController ↔ /easycompany/hierarchy/employeeList*, InsertEmployeeController ↔ /easycompany/hierarchy/insertEmployee* 과 같이 url mapping이 이루어 진다.

SimpleUrlHandlerMapping

SimpleUrlHandlerMapping은 Ant-Style 패턴 매칭을 지원하며, 하나의 Controller에 여러 URL을 mapping 할 수 있다. proerty의 key 값에 URL 패턴을 지정하고 value에는 Controller의 id 혹은 이름을 지정한다.

<beans ...>
...
         <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
                <property name="mappings">
                        <props>
                                <prop key="/employeeList.do">employeeListController</prop>
                                <prop key="/insertEmployee.do">insertEmployeeController</prop>
                                <prop key="/updateEmployee.do">updateEmployeeController</prop>
                                <prop key="/loginProcess.do">loginController</prop>
                                <prop key="/**/login.do">staticPageController</prop>
                                <prop key="/static/*.html">staticPageController</prop>
                        </props>
                </property>
         </bean>
 
         <bean id="loginController" class="com.easycompany.controller.hierarchy.LoginController"/>
         <bean id="employeeListController" class="com.easycompany.controller.hierarchy.EmployeeListController" />
         <bean id="insertEmployeeController" class="com.easycompany.controller.hierarchy.InsertEmployeeController" />
         <bean id="updateEmployeeController" class="com.easycompany.controller.hierarchy.UpdateEmployeeController" />
         <bean id="staticPageController" class="org.springframework.web.servlet.mvc.UrlFilenameViewController" />
...
</beans>

SimpleUrlHandlerMapping을 사용하면 Interceptor를 특정 URL 단위로 적용하는게 가능하다. 프로퍼티 interceptors에 적용하려는 Interceptor들을 리스트로 선언해주면 된다. URL /employeeList.do, /insertEmployee.do, /updateEmployee.do 요청에 대해서 사용자 인증여부를 interceptor로 검증한다고 하면,아래와 같이 정의한다.

<beans ...>
...
         <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
                <property name="interceptors">
                     <list>
                        <ref local="authenticInterceptor"/>
                     </list>
                </property>
                <property name="mappings">
                        <props>
                                <prop key="/employeeList.do">employeeListController</prop>
                                <prop key="/insertEmployee.do">insertEmployeeController</prop>
                                <prop key="/updateEmployee.do">updateEmployeeController</prop>
                        </props>
                </property>
         </bean>
         <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
                <property name="mappings">
                        <props>                                
                                <prop key="/**/login.do">staticPageController</prop>
                                <prop key="/static/*.html">staticPageController</prop>
                        </props>
                </property>
         </bean>
 
         <bean id="loginController" class="com.easycompany.controller.hierarchy.LoginController"/>
         <bean id="employeeListController" class="com.easycompany.controller.hierarchy.EmployeeListController" />
         <bean id="insertEmployeeController" class="com.easycompany.controller.hierarchy.InsertEmployeeController" />
         <bean id="updateEmployeeController" class="com.easycompany.controller.hierarchy.UpdateEmployeeController" />
         <bean id="staticPageController" class="org.springframework.web.servlet.mvc.UrlFilenameViewController" />
 
         <bean id="authenticInterceptor" class="com.easycompany.interceptor.AuthenticInterceptor" />
...
</beans>

SimpleUrlAnnotationHandlerMapping (@deprecated)

  • Spring 3부터 <mvc:interceptors> element를 통해 SimpleUrlAnnotationHandlerMapping과 동일한 기능을 제공하므로 deprecated되었다.

DefaultAnnotationHandlerMapping에 interceptor를 등록하면, 모든 @Controller에 interceptor가 적용되는 문제점이 있다. SimpleUrlAnnotationHandlerMapping은 @Controller 사용시에 url 단위로 Interceptor를 적용하기 위해 개발되었다. Spring 3부터 <mvc:interceptors>를 이용하여 동일한 기능을 제공하므로 현재 SimpleUrlAnnotationHandlerMapping은 deprecated되었다. 그러나 이전 버전은 해당기능을 제공하지 않는다.

SimpleUrlAnnotationHandlerMapping은 아래와 같은 3가지 사항이 고려됬다.

  • HandlerMapping:Interceptors 관계의 스프링 구조를 깨뜨리지 말자. (ex. Controller:Interceptor (X))
  • 쉬운 사용을 위해 기존의 HandlerMapping과 비슷한 방식의 사용법을 선택하자. (ex.SimpleUrlHandlerMapping)
  • 최소한의 커스터마이징을 하자. → 짧은 시간… 또한 추후 deprecated시에 시스템에 영향을 최소화 하기 위해.

웹 어플리케이션이 초기 구동될때, DefaultAnnotationHandlerMapping은 2가지 주요한 작업을 한다. (다른 HandlerMapping도 유사한 작업을 한다.)

  1. @RequestMapping의 url 정보를 읽어 들여 해당 Controller와 url간의 매핑 작업.
  2. 설정된 Interceptor들에 대한 정보를 읽어 들임.

1번 작업은 DefaultAnnotationHandlerMapping의 상위 클래스인 AbstractDetectingUrlHandlerMapping에서 이루어 지는데, 맵핑을 위한 url리스트를 가져오는 determineUrlsForHandler 메소드는 하위 클래스에서 구현하도록 abstract 선언 되어 있다.

public abstract class AbstractDetectingUrlHandlerMapping extends AbstractUrlHandlerMapping {
...
	protected void detectHandlers() throws BeansException {
		if (logger.isDebugEnabled()) {
			logger.debug("Looking for URL mappings in application context: " + getApplicationContext());
		}
		String[] beanNames = (this.detectHandlersInAncestorContexts ?
				BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
				getApplicationContext().getBeanNamesForType(Object.class));
 
		// Take any bean name that we can determine URLs for.
		for (int i = 0; i < beanNames.length; i++) {
			String beanName = beanNames[i];
			String[] urls = determineUrlsForHandler(beanName);
			if (!ObjectUtils.isEmpty(urls)) {
				// URL paths found: Let's consider it a handler.
				registerHandler(urls, beanName);
			}
			else {
				if (logger.isDebugEnabled()) {
					logger.debug("Rejected bean name '" + beanNames[i] + "': no URL paths identified");
				}
			}
		}
	}
	protected abstract String[] determineUrlsForHandler(String beanName);
}

DefaultAnnotationHandlerMapping의 determineUrlsForHandler 메소드는 @RequestMapping의 url 리스트를 전부 가져오기 때문에, 빈 설정 파일에 정의한 url 리스트만 가져오도록 SimpleUrlAnnotationHandlerMapping에서 determineUrlsForHandler 메소드를 구현 한다.

package egovframework.rte.ptl.mvc.handler;
...
public class SimpleUrlAnnotationHandlerMapping extends DefaultAnnotationHandlerMapping {
 
	//url리스트, 중복값을 허용하지 않음으로 Set 객체에 담는다.
	private Set<String> urls;
 
	public void setUrls(Set<String> urls) {
		this.urls = urls;
	}
 
	/**
	 * @RequestMapping로 선언된 url중에 프로퍼티 urls에 정의된 url만 remapping해 return
	 * url mapping시에는 PathMatcher를 사용하는데, 별도로 등록한 PathMatcher가 없다면 AntPathMatcher를 사용한다.
	 * @param urlsArray - @RequestMapping로 선언된 url list
	 * @return urlsArray중에 설정된 url을 필터링해서 return.
	 */
	private String[] remappingUrls(String[] urlsArray) {
 
		if (urlsArray==null) {
			return null;
		}
 
		ArrayList<String> remappedUrls = new ArrayList<String>();
 
		for(Iterator<String> it = this.urls.iterator(); it.hasNext();) {
			String urlPattern = (String) it.next();
			for(int i=0;i<urlsArray.length;i++){
				if(getPathMatcher().matchStart(urlPattern, urlsArray[i])){					
					remappedUrls.add(urlsArray[i]);
				}
			}
		}
 
		return (String[]) remappedUrls.toArray(new String[remappedUrls.size()]);		
	}
 
	/**
	 * @RequestMapping로 선언된 url을 필터링하기 위해
	 * org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping의 
	 * 메소드 protected String[] determineUrlsForHandler(String beanName)를 override.
	 * 
	 * @param beanName - the name of the candidate bean
	 * @return 빈에 해당하는 URL list
	 */
	protected String[] determineUrlsForHandler(String beanName) {
		return remappingUrls(super.determineUrlsForHandler(beanName));
	}	
}

인터셉터를 적용할 url들을 프로퍼티 urls에 선언하면 되며, Ant-style의 패턴 매칭이 지원된다. SimpleUrlAnnotationHandlerMapping은 선언된 url만을 Controller와 매핑처리한다. 따라서, 아래와 같이 선언된 DefaultAnnotationHandlerMapping와 같이 선언되어야 하며, 우선순위는 SimpleUrlAnnotationHandlerMapping이 높아야 한다.

<bean id="selectAnnotaionMapper"
    class="egovframework.rte.ptl.mvc.handler.SimpleUrlAnnotationHandlerMapping"
    p:order="1">
    <property name="interceptors">
        <list>
            <ref local="authenticInterceptor"/>
        </list>
    </property>
    <property name="urls">
        <set>
            <value>/*Employee.do</value>
            <value>/employeeList.do</value>
        </set>
    </property>
</bean>

<bean id="annotationMapper" 
    class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" 
    p:order="2"/>

<bean id="authenticInterceptor" class="com.easycompany.interceptor.AuthenticInterceptor" />

참고자료

  • The Spring Framework - Reference Documentation 2.5.6
  • Spring Framework API Documentation 2.5.6

3.5 - Spring MVC Tag Configuration

Spring 3부터 mvc태그를 통하여 Controller처리를 위한 설정을 쉽게 하도록 Spring mvc 네임스페이스를 제공한다.

Spring MVC Tag Configuration

개요

Spring 3부터 mvc태그를 통하여 Controller처리를 위한 설정을 쉽게 하도록 Spring mvc 네임스페이스를 제공한다.

설명

mvc:annotation-driven

Spring 3.0부터 제공하는 mvc 태그 설정이다. Annotation기반의 Controller호출 설정과 필요한 bean설정을 편리하게 하도록 만들어졌다. 그러나 내부 수정이 어렵기 때문에 mvc:annotation-driven에서 제공하는 기능에 대하여 잘 숙지하고 변경이 불가능 한 경우에는 mvc:annotation-driven을 쓰지 않고 필요한 bean을 수동으로 넣어줘야하는 경우도 있다. mvc:annotation-driven에서 쓰는 bean설정을 중복으로 쓰지 않도록 주의한다.

mvc:annotation-driven에서 제공하는 기능

  • RequestMappingHandlerMapping bean등록(기존 DefaultAnnotationHandlerMapping)
  • RequestMappingHandlerAdapter bean등록(기존 AnnotationMethodHandlerAdapter)
    • customArgumentResolvers, customReturnValueHandlers 추가 가능
  • JSR-303의 검증용 어노테이션(@Valid)를 사용할 수 있도록 LocalValidatorFactoryBean bean설정 (JSR-303지원 라이브러리 존재 시)
  • RequestMappingHandlerAdapter의 messageConverters프로퍼티로 메시지 컨버터들 등록

(다음 설정과 동일한 동작을 한다.)

<bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter" />
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
  <property name="writeAcceptCharset" value="false" />
</bean>
<bean class="org.springframework.http.converter.ResourceHttpMessageConverter" />
<bean class="org.springframework.http.converter.xml.SourceHttpMessageConverter" />
<bean class="org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter" />
 
<!-- jaxb2라이브러리 존재시 -->
<bean class="org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter" />
 
<!-- jackson 라이브러리 존재시 -->
<bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
 
<!-- rome 라이브러리 존재시 -->
<bean class="org.springframework.http.converter.feed.AtomFeedHttpMessageConverter" />
<bean class="org.springframework.http.converter.feed.RssChannelHttpMessageConverter" />
  • 디폴트 validator로 어노테이션 방식의 포맷터를 지원하는 FormattingConversionServiceFactoryBean를 제공하며 validator를 추가 설정하도록 지원
  • 디폴트 컨버전 서비스로 FormattingConversionServiceFactoryBean을 제공하며 conversion-service를 추가 설정하도록 지원
  • ConversionServiceExposingInterceptor를 등록하여 JSP의 <spring:eval>에서 ConversionService를 적용되도록 지원

mvc:annotation-driven 설정 시 주의해야할 점

  1. <mvc:annotation-driven>을 사용할 때는 직접 RequestMappingHandlerAdapter를 등록해주어서는 안되며 직접 등록이 필요한 경우에는 <mvc:annotation-driven>을 설정하지 않고 각각의 필요한 설정을 수동으로 해주어야 한다. 전자정부 프레임워크 3.0에서는 Controller의 파라미터로 CommandMap을 쓰기 위하여 RequestMappingHandlerAdapter를 상속받은 EgovRequestMappingHandlerAdapter를 만들었으며, CommandMap을 써야하는 경우 <mvc:annotation-driven>을 설정하지 않고 EgovRequestMappingHandlerAdapter를 직접 선언하도록 가이드하고 있다.
  2. RequestMappingHandlerAdapter의 customArgumentResolvers 속성에 pring 3.1이전 버전은 WebArgumentResolver인터페이스를 구현해야 했으나 HandlerMethodArgumentResolver인터페이스를 구현한 ArgumentResolver가 설정해야 한다. <mvc:annotation-driven> 내부 설정으로 WebArgumentResolver인터페이스 구현체를 쓸 수 있으나 이는 RequestMappingHandlerAdapter가 처리하는 것이 아니라 ServletWebArgumentResolverAdapter에서 호환하도록 변경해주는 것이다.
  3. customReturnValueHandlers의 속성은 (Spring3.1이전버전의 customModelAndViewResolvers속성에서 이름변경) 기존에는 ModelAndViewResolver인터페이스의 구현체를 설정하였으나 Spring 3.1부터 HandlerMethodReturnValueHandler인터페이스의 구현체를 설정해야 한다.

mvc:interceptors

기존 HandlerMapping에는 Interceptor를 모든 url에 일괄적으로만 적용할 수 있었기 때문에 전자정부 프레임워크에서는 SimpleUrlAnnotationHandlerMapping을 제공하여 url별로 Interceptor를 걸 수 있도록 하였다. 그러나 Spring 3부터 제공하는 mvc:interceptors태그를 통해 url마다 Interceptor를 적용할 수 있도록 Spring mvc태그 스키마에서 제공하고 있다. 따라서 SimpleUrlAnnotationHandlerMapping은 deprecated되었으며 url별로 Interceptor를 적용하기 위해서는 mvc:interceptors태그를 사용하도록 한다.

Interceptor를 일괄 적용하기 위해서는 다음의 예와 같이 사용한다.

<mvc:interceptors>
    <bean class="egov.interceptors.EgovInterceptor"/>
</mvc:interceptors>

특정 패턴의 url에만 인터셉터를 적용하기 위해서는 <mvc:interceptors>태그 내부에 <mvc:interceptor>를 사용한다. 만약 /egov/sample로 시작되는 URL요청에만 인터셉터를 정의하기 위해서는 다음과 같이 사용할 수 있다.

<mvc:interceptors>
    <mvc:interceptor>
      <mvc:mapping path="/egov/sample/*"/>
      <bean class="egov.interceptors.EgovInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

또한 interceptor에 특정 URL Pattern을 제외하여 맵핑하는 기능도 지원하고 있다. 이 때는 <mvc:interceptor>내부에서 <exclude-mapping>태그를 사용한다. 만약 /egov/로 시작하는 URL중 /egov/admin/으로 시작하는 URL에 interceptor맵핑을 제외하고 싶으면 다음과 같이 사용한다.

<exclude-mapping> 태그는 spring 3.2 버전 부터 사용 가능 합니다.

<mvc:interceptors>
    <mvc:interceptor>
      <mvc:mapping path="/egov/**"/>
      <mvc:exclude-mapping path="/egov/admin/**"/>
      <bean class="egov.interceptors.EgovInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>

mvc:view-controller

Controller에서 별다른 로직 없이 View를 지정하여 DispatcherServlet에 넘겨주는 작업만 하는 경우가 많다. 이럴 때 사용할 수 있는 것이 바로 <mvc:view-controller>태그이다. <mvc:view-controller>태그를 설정하여 매핑할 URL패턴과 View이름만 넣어주면 해당 URL을 매핑하고 설정한 view를 리턴하는 ParameterizableViewController가 자동으로 등록된다.

만약 ”/“URL을 요청받았을 경우 “index”를 View이름으로 리턴하기 위해서는 다음과 같이 사용한다.

<mvc:view-controller path="/" view-name="index"/>

InternalResourceViewResolver의 prefix를 /WEB-INF/views/로 정하고 suffix를 .jsp로 정하였다면 / URL이 요청되었을 때 /WEB-INF/views/index.jsp view를 호출하게 된다.

참고자료

3.6 - Controller

DispatcherServlet은 HandlerMapping을 통해 요청을 처리할 Controller를 결정하고, 해당 Controller는 요청을 처리하여 데이터를 Model 객체에 반영한다. Spring MVC는 다양한 Controller들을 제공하며, 이들은 대부분 org.springframework.web.servlet.mvc.Controller 인터페이스를 구현한 클래스들이다.

Controller

개요

DispatcherServlet은 HandlerMapping를 이용해서 해당 요청을 처리할 Controller를 결정한다. 이 Controller는 요청에 대해서 처리를 하고 데이터를 Model 객체에 반영한다. Spring MVC는 다양한 종류의 Controller를 제공하는데, 데이터 바인딩이나 폼 처리 또는 멀티 액션등의 편의 기능을 제공한다. 이 Controller들은 org.springframework.web.servlet.mvc.Controller 인터페이스를 구현한 클래스들이다.(@Controller는 예외다. 여기서는 @Controller에 대한 설명은 제외한다.) eclipse에서 인터페이스 Controller를 Hierarchy View에서 열어보면 아래와 같은 구조를 보여준다.

Controller preview

이 중 주요 Controller의 용도 및 특징을 표로 나타내면 아래와 같다.

클래스용도 및 특징
Controller기본적인 Controller 인터페이스이다. Struts의 Action과 비교될 수 있다.
Spring에서는 Controller 작성시에 직접 Controller 인터페이스를 구현하지 말고, 아래의 구현 클래스를 확장해서 작성할것을 권장한다.
AbstractController웹 요청과 응답을 처리하는 기본적인 Controller이다.
WebContentGenerator를 상속받기 때문에, HTTP 메소드(POST,GET) 지정, 세션 필수 여부등의 편의기능을 추가로 받는다.
AbstractCommandControllerHttpServletRequest의 파라미터를 동적으로 데이터 객체(Command)에 바인딩 할 수 있다.
하지만 HTML 폼 처리시엔 SimpleFormController을 사용하라.
SimpleFormControllerHTML 폼 처리시에 사용하는 Controller이다.
AbstractCommandController처럼 HttpServletRequest의 파라미터와 Command 객체를 바인딩할뿐 아니라,
입력폼에 필요한 데이터를 채워 보여주거나(referenceData, formBackingObject), 일반적인 폼 처리 시나리오에 따른 view 분기(formView, successView) 등의 편의 기능을 제공한다.
MultiActionController연관된 여러 액션을 한 Controller에서 처리할때 사용하는 Controller이다.
UrlFilenameViewControllerController에서 처리 로직이 없이 바로 view로 이동하는 경우에 사용한다.

설명

AbstractController

단순히 요청을 처리하고 그 결과를 ModelAndView 객체에 반영하는 작업을 할 때는 AbstractController을 상속한 Controller를 구현하면 된다. 구현 Controller에서는 AbstractController의 추상 메소드인 handleRequestInternal을 구현하면 된다.

protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception;

Controller를 작성할때 인터페이스 Controller를 바로 구현하는 대신, AbstractController를 상속받아 구현하면 특정 HTTP 메소드(GET,POST)에 대한 필터링이나 세션 필수 체크등의 편의 기능을 제공받는다. AbstractController의 작업 흐름은 아래와 같다.

  1. DispatcherServlet에 의해 handleRequest()가 호출된다.
  2. 특정 HTTP 메소드(GET,POST)에 대한 필터링을 수행한다.
  3. 세션필수여부값이 true이면 HttpServletRequest 객체에서 세션을 꺼낸다.
  4. cacheSeconds 프로퍼티에 설정된 값에 따라 캐시 헤더를 설정한다.
  5. 추상 메소드인 handleRequestInternal()을 호출한다. AbstractController를 상속받은 구현, Controller 클래스를 작성했다면 구현된 handleRequestInternal() 메소드가 실행된다.

관련 프로퍼티를 정리하면 아래와 같다.

이름기본값설명
supportedMethodsGET,POSTController가 지원하는 HTTP 메소드 리스트(GET, POST and PUT)로 콤마(,)로 구분한다.
requireSessionfalseController에서 요청 처리시에 session이 반드시 필요한지 여부이다. 값이 true인데 세션이 없다면 ServletException이 발생한다.
cacheSeconds-1응답의 캐시 헤더에 설정하는 시간값으로 단위는 초단위이다.
값이 0이면 캐시를 수행하지 않는 헤더를 갖게 되며, -1(기본값)이면 어떤 헤더도 생성하지 않으며, 양수값을 설정하면 설정한 값(초)만큼 내용을 캐싱을 수행하는 헤더를 생성한다.
synchronizeOnSessionfalse메소드 handleRequestInternal()을 호출할때 세션(HttpSession)에 동기화(synchronized)해서 호출할지 여부이다. 만일 세션이 없다면 아무 영향이 없다.

예제

사용자 인증처리를 위해 아이디와 패스워드를 입력받는 페이지 예제

web-servlet-controller-ex-login-form

<%@ page contentType="text/html; charset=UTF-8"%>
<html>
	<head>
	<title>Login Page</title>
	<link type="text/css" rel="stylesheet" href="scripts/easycompany.css" />
</head>
<body>
	<form action="/easycompany/loginProcess.do" method="post">
		아이디 : <input type=text name="id"> 패스워드: <input type=password name="password"> <input type=submit value="로그인">
	</form>
</body>
</html>

로그인 처리를 하는 LoginController를 AbstractController를 확장해서 작성해 보자. 먼저 아래와 같이 빈설정을 하고

<bean name="/loginProcess.do" class="com.easycompany.controller.hierarchy.LoginController"
	p:loginService-ref="loginService"
	p:supportedMethods="POST"/> <!--HTTP 메소드가 POST일때만 처리한다-->

메소드 handleRequestInternal()에 실제 구현 로직을 넣어 준다.

package com.easycompany.controller.hierarchy;
...
public class LoginController extends AbstractController{
 
	private LoginService loginService;
 
	public void setLoginService(LoginService loginService){
		this.loginService = loginService;
	}
 
	/**
	 *  AbstractController의 추상 메소드인 handleRequestInternal의 구현 메소드이다.
	 *  사용자로 부터 아이디, 패스워드를 입력받아 인증 성공이면 세션 객체에 계정정보를 담고 사원정보리스트 페이지로 포워딩한다.
	 *  인증에 실패하면 로그인 페이지로 다시 리턴한다.
	 */	
	@Override
	protected ModelAndView handleRequestInternal(HttpServletRequest request,
			HttpServletResponse response) throws Exception {
 
		String id = request.getParameter("id"); //아이디
		String password = request.getParameter("password"); //패스워드
 
		//로그인 인증을 처리한 후, 로그인 성공이면 Account 객체에 계정 관련정보를 리턴한다. 로그인 실패이면 null 리턴.  
		Account account = (Account) loginService.authenticate(id,password);
 
		if (account != null) { //로그인 성공
			request.getSession().setAttribute("UserAccount", account); //계정정보를 세션에 저장.
			return new ModelAndView("redirect:/employeeList.do"); //사원리스트 페이지로 이동.
		} else { //실패
			return new ModelAndView("login"); //로그인페이지로 이동
		}
	}
}

AbstractCommandController

AbstractCommandController는 요청 파라미터값을 커맨드(Command) 클래스의 필드값과 자동으로 바인딩할 때 사용된다. 커맨드 클래스는 일반적인 JavaBean이면 되는데, ActionForm 같이 프레임워크에 종속적인 구조의 폼 클래스를 사용해야 하는 Struts와의 차이점이라 할 수 있다. 파라미터와 커맨드 클래스의 데이터 바인딩은 일반적으로 알려진 JavaBeans 프로퍼티 표시법을 따른다. firstName 이란 이름의 파라미터가 있다면 커맨드 클래스의 setFirstName([value]) 메소드를 찾아 값을 바인딩한다. 파라미터 address.city 는 커맨드 클래스의 getAddress().setCity([value]) 메소드를 찾아 값을 바인딩한다. 이 기능은 HTML 폼 처리에 유용한 편의 기능이지만, 일반적으로 HTML 폼 처리에는 AbstractCommandController대신 SimpleFormController를 사용한다. AbstractCommandController을 상속받는 구현 Controller에서는 추상메소드 handle()을 구현하면 된다.

protected abstract ModelAndView handle(
			HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) throws Exception;

예제

사원번호,부서번호,사원이름등의 검색 조건에 따라 사원리스트를 보여주는 페이지 예제

web-servlet-controller-ex-employee-list

검색 조건을 담는 빈은 아래와 같다.

package com.easycompany.domain;
public class SearchCriteria {
 
	private String searchEid;
	private String searchDid;
	private String searchName;
 
    // 위의 변수들에 대한 getter/setter...
}

검색 조건을 화면으로 부터 입력받아 검색 조건 빈(bean)인 SearchCriteria에 담아서 서비스에 넘겨주고 리스트로 결과를 받아오는 EmployeeListController 작성해 보자. EmployeeListController를 AbstractController를 이용해 만든다면, 파라미터의 값을 꺼내고 값을 객체에 담는 코드를 직접 작성해야 한다.

package com.easycompany.controller.hierarchy;
...
public class EmployeeListController extends AbstractController{
	....
        protected ModelAndView handleRequestInternal(HttpServletRequest request,
			HttpServletResponse response) throws Exception {
 
		//request 객체의 파라미터값을 꺼내서
		String searchEid = request.getParameter("searchEid"); //사원번호
		String searchDid = request.getParameter("searchDid"); //부서번호
		String searchName = request.getParameter("searchName"); //사원이름
		//객체에 저장한다.
		SearchCriteria searchCriteria = new SearchCriteria();
		searchCriteria.setSearchEid(searchEid);
		searchCriteria.setSearchDid(searchDid);
		searchCriteria.setSearchName(searchName);
 
		List<Employee> employeelist = employeeService.getAllEmployees(searchCriteria);
 
		ModelAndView modelview = new ModelAndView();
		modelview.addObject("employeelist", employeelist);
		modelview.addObject("searchCriteria", searchCriteria);
		modelview.setViewName("employeelist");
 
		return modelview;
	}
}

맵핑해야할 파라미터가 많다면 상당히 번거로운 작업이고, 단순 작업 코드의 라인이 길어져 코드의 가독성도 떨어진다. EmployeeListController를 AbstractCommandController을 상속받아 구현해 보면 아래와 같이 변경될 것이다.

package com.easycompany.controller.hierarchy;
...
public class EmployeeListController extends AbstractCommandController{
	public EmployeeListController(){
		//Command 객체에 대한 선언. 빈 설정 파일에 Command 객체에 대한 선언이 있다면 이 코드는 필요없다.
		setCommandClass(SearchCriteria.class);
		setCommandName("searchCriteria");
	}
	....
 
	@Override
	protected ModelAndView handle(HttpServletRequest request,
			HttpServletResponse response, Object command, BindException errors)
			throws Exception {
		//이미 파라미터와 Command 객체의 바인딩이 되어 있다. 
		SearchCriteria searchCriteria = (SearchCriteria)command;
 
		List<Employee> employeelist = employeeService.getAllEmployees(searchCriteria);
 
		ModelAndView modelview = new ModelAndView();
		modelview.addObject("employeelist", employeelist);
		modelview.addObject("searchCriteria", searchCriteria);
		modelview.setViewName("employeelist");
 
		return modelview;
	}
}

커맨드 클래스 설정은 setCommandClass,setCommandName 메소드 대신에 빈 설정 파일에 정의할 수 있다.

<bean id="employeeListController" class="com.easycompany.controller.hierarchy.EmployeeListController"
	p:employeeService-ref="employeeService"
	p:commandName="searchCriteria"
	p:commandClass="com.easycompany.domain.SearchCriteria"/>

이 데이터 바인딩은 spring의 폼 태그 <form:form> 와 함께 쓰면 더욱 편리하게 사용할 수 있다. 폼 태그의 변수 commandName은 커맨드 클래스의 이름과 일치해야 한다. 커맨드 객체에 사원번호, 부서번호등의 값이 들어 있다면 JSP는 커맨드 객체의 필드값과 폼필드값을 자동으로 바인딩하여 보여주게 된다.

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
...
<form:form commandName="searchCriteria" action="/easycompany/employeeList.do">
<table width="50%" border="1">
	<tr>
		<td>사원번호 : <form:input path="searchEid"/></td>
		<td>부서번호 : <form:input path="searchDid"/></td>
		<td>이름 : <form:input path="searchName"/></td>
		<td><input type="submit" value="검색" onclick="this.disabled=true,this.form.submit();" /></td>
	</tr>	
</table>
</form:form>
<table>
	<tr>
		<th></th>
		<th>사원번호</th>
		<th>부서번호</th>
		<th>이름</th>
		<th>나이</th>
		<th>이메일</th>
	</tr>	
<c:forEach items="${employeelist}" var="empinfo">	
	<tr>
		<td></td>
		<td><a href="javascript:getEmployeeInfo('${empinfo.employeeid}')">${empinfo.employeeid}</a></td>
		<td>${empinfo.departmentid}</td>
		<td>${empinfo.name}</td>
		<td>${empinfo.age}</td>
		<td>${empinfo.email}</td>
	</tr>
</c:forEach>
</table>
...

SimpleFormController

HTML 폼을 보여주거나 전송(submission)하는 등에 폼처리를 다루는 Controller를 작성 한다면, SimpleFormController를 상속한 Controller를 구현하면 된다. SimpleFormController는 상위 클래스인 BaseCommandController와 AbstractFormController가 제공하는 파라미터와 커맨드(폼) 클래스의 데이터 바인딩, 세션 폼 모드, 입력값 검증(validation), 입력폼에 초기 데이터 세팅등의 편의 기능을 그대로 사용하면서 폼 전송시에 결과에 따른 화면 분기(formView, successView)등 편의 기능을 추가로 제공한다. 폼을 보여주고 전송하는 Controller를 각각 만들지 않고 하나로 만들 수 있다. SimpleFormController의 작업 흐름을 보려면 상위 클래스인 AbstractFormController의 handleRequestInternal() 메소드를 참고하면 되는데, 작업 흐름은 아래와 같다.

  • GET 방식 호출

    • Controller가 폼 페이지에 대한 요청을 받는다. (GET 방식 호출)
    • formBackingObject() 메소드는 기본적으로 요청에 대한 커맨드 객체를 생성해서 반환한다. 일반적으로는 formBackingObject를 오버라이딩해서 GET 방식 호출시에 폼에 채우고자 하는 기본값을 가져오는 로직을 넣는다.
    • initBinder() 메소드가 실행되는데, 커맨드 클래스의 특정 필드에 대해서 커스텀 에디터를 사용할수 있도록 한다.
    • 프로퍼티 bindOnNewForm이 true이면, 초기 요청 파라미터들을 가지고 새로운 커맨드 객체에 값을 채우는데 ServletRequestDataBinder가 적용되며, onBindOnNewForm() 콜백 메소드가 호출된다.
    • showForm() 메소드는 referenceData()를 호출해서 폼 페이지에서 보여주고자 하는 참조 데이터(주로 셀렉트박스, 체크박스 같은 유형)를 ModelAndView에 저장한다.
    • formView에서는 Model 데이터를 바탕으로 폼에 필요한 데이터를 채워서 표시한다.
  • POST 방식 호출

    • 사용자가 폼데이터를 전송한다(submit). (POST 방식 호출)
    • getCommand() 메소드가 커맨드 객체를 반환하는데, 만일 sessionForm이 false이면 formBackingObject() 메소드를 호출해서 커맨드 클래스의 인스턴스를 반환하고, sessionForm이 true이면 세션에서 커맨드 객체를 꺼내서 반환한다. 만일 해당 객체를 세션에서 찾지 못하면 handleInvalidSubmit() 메소드를 호출한다. handleInvalidSubmit()는 새로운 커맨드 객체를 생성하고 다시 폼 전송을 시도한다.
    • 요청 파라미터들로 커맨드 객체를 채우기 위해 ServletRequestDataBinder가 사용된다.
    • onBind() 메소드를 호출한다. 유효성검사(validation) 수행전에 필요한 작업들을 수행할 수 있다.
    • 프로퍼티 validateOnBinding값이 true이면, 등록된 Validator가 호출된다. Validator는 커맨드 객체의 필드값에 대한 유효성을 검사한다.
    • onBindAndValidate() 메소드를 호출한다. 여기서 바인딩과 유효성 검사 이후 사용자 정의 작업을 수행할 수 있다.
    • processFormSubmission() 메소드에서 전송을 처리한다. 유효성검사 결과 에러가 있는 경우 showForm() 메소드가 호출되어 다시 formView로 이동하고 에러가 없으면 onSubmit() 메소드가 수행되면서 폼 제출이 된다.
    • 폼 제출이 성공하면 successView로 이동한다.

관련 프로퍼티는 아래와 같다.

이름기본값설명해당클래스
commandNamecommand커맨드 클래스의 이름(별칭)BaseCommandController
commandClassnull요청 파라미터와 데이터 바인딩하게 될 커맨드 클래스BaseCommandController
validatorsnull커맨드 객체의 데이터 유효성검사를 수행할 Validator 빈의 배열BaseCommandController
validatornullValidator가 한개인 경우 사용.BaseCommandController
validateOnBindingtrue유효성검사를 수행할지 여부. true이면 수행한다.BaseCommandController
bindOnNewFormfalse새로운 폼이 보여지는 시점에서 데이터 바인딩을 할지 여부.AbstractFormController
sessionFormfalse커맨드 객체를 세션에 저장하여 사용할지 여부.AbstractFormController
formViewnull사용자가 입력하는 폼페이지나 유효성검사시에 에러났을 경우에 사용하는 뷰를 표시한다.SimpleFormController
successViewnull폼 제출이 성공했을때 보여줄 뷰를 표시한다.SimpleFormController

예제

부서 정보 수정 페이지(/easycompany/webapp/WEB-INF/jsp/modifydepartment.jsp) 예제 이 페이지의 처리 흐름은 아래와 같다.

  • 사용자에게 입력폼페이지를 보여주되 기존의 부서정보를 채워서 보여준다. → 위에서 언급한 GET 방식 호출시의 프로세스에 따라 처리 된다.
  • 사용자는 수정할 내용을 수정한 후에 저장 버튼을 누른다. → POST 방식 호출시의 프로세스에 따라 처리 된다.
    • 저장에 실패하거나 입력값 검증에 문제가 있으면 다시 초기 입력 폼페이지로 이동한다.
    • 저장에 성공하면 부서 정보 리스트페이지로 이동한다.

web-servlet-controller-ex-employee-reg

<form:form commandName="department">
<table>
	<tr>
		<th>부서번호</th>
		<td><c:out value="${department.deptid}"/></td>
	</tr>
	<tr>
		<th>부서이름</th>
		<td><form:input path="deptname" size="20"/></td>
	</tr>
	<tr>
		<th>상위부서</th>
		<td>
			<form:select path="superdeptid">
				<option value="">상위부서를 선택하세요.</option>
				<form:options items="${deptInfoOneDepthCategory}" />
			</form:select>
		</td>
	</tr>
	<tr>
		<th>설명</th>
		<td><form:textarea path="description" rows="10" cols="40"/></td>
	</tr>	
</table>
<table width="80%" border="1">
	<tr>
		<td>
		<input type="submit" value="저장"/>		
		<input type="button" value="리스트페이지" onclick="location.href='/easycompany/departmentList.do?depth=1'"/>
		</td>
	</tr>
</table>
</form:form>

처리를 담당할 UpdateDepartmentController를 빈 설정 파일(xxx-servlet.xml)에 아래와 같이 등록한다.

<bean id="updateDepartmentController" class="com.easycompany.controller.hierarchy.UpdateDepartmentController"
        p:departmentService-ref="departmentService"
        p:commandName="department"
        p:commandClass="com.easycompany.domain.Department"
        p:formView="modifydepartment"
        p:successView="redirect:/departmentList.do?depth=1"/>
formBackingObject 메소드
protected Object formBackingObject(HttpServletRequest request) throws Exception

일반적으로 수정 폼페이지는 기존의 데이터를 폼에 채우고 사용자가 원하는 부분을 수정한 후에 전송(submit)하는데, 기존 데이터를 불러와 폼에 채우는 역할을 formBackingObject 메소드가 담당한다. formBackingObject 메소드는 커맨드 객체를 생성해서 리턴하는데, 필요에 따라 이 메소드를 오버라이드해서 필요한 데이터를 커맨드 객체에 채워 주면 된다. 부서 정보 수정 페이지에서 기존의 부서 정보를 채워서 보여 주는 부분을 먼저 작성해 보자. 오버라이드한 메소드에HTTP GET 메소드 요청이면 파라미터에 있는 부서 아이디로 부서 정보 테이블을 조회해서 결과를 객체 Department에 담아 반환하는 로직을 추가해 보자.

package com.easycompany.controller.hierarchy;
...
 
public class UpdateDepartmentController extends SimpleFormController{
 
	private DepartmentService departmentService;
 
	public void setDepartmentService(DepartmentService departmentService){
		this.departmentService = departmentService;
	}
 
	@Override
	protected Object formBackingObject(HttpServletRequest request) throws Exception {
		if(!isFormSubmission(request)){	// GET 요청이면
			String deptid = request.getParameter("deptid");
			Department department = departmentService.getDepartmentInfoById(deptid);//부서 아이디로 DB를 조회한 결과가 커맨드 객체 반영.
			return department;
		}else{	// POST 요청이면
			//AbstractFormController의 formBackingObject을 호출하면 요청객체의 파라미터와 설정된 커맨드 객체간에 기본적인 데이터 바인딩이 이루어 진다.
			return super.formBackingObject(request);
		}
	}
        ...	
}
referenceData 메소드
protected Map referenceData(HttpServletRequest request) throws Exception
protected Map referenceData(HttpServletRequest request, Object command, Errors errors) throws Exception

폼 페이지에 미리 보여 주어야 할 데이터중에 커맨드 객체에 포함하기 어려운 경우가 있다. 부서 정보 수정 페이지에 보면 상위 부서 정보가 셀렉트박스로 되어 있는데, 커맨드 객체인 부서(Department)객체에는 해당 부서의 상위부서번호만 있을 뿐 이 회사에 어떤 상위부서들이 있는 지에 대한 정보는 없다. 커맨드 객체에 없지만 페이지에 필요한 이런 참조성 데이터들을 사용하기 위해서 referenceData 메소드를 사용하면 된다. referenceData 메소드는 맵 객체를 반환하는데 이 맵은 모델 객체에 담겨 ModelAndView에 저장된다. 우리는 그저 referenceData 메소드를 오버라이드해서 참조성 데이터를 맵 객체에 넣어 주면 된다.

package com.easycompany.controller.hierarchy;
...
public class UpdateDepartmentController extends SimpleFormController{
 
	private DepartmentService departmentService;
 
	public void setDepartmentService(DepartmentService departmentService){
		this.departmentService = departmentService;
	}
 
	@Override
	protected Map referenceData(HttpServletRequest request, Object command, Errors errors) throws Exception{
 
		Map param = new HashMap();
		param.put("depth", "1");		
		Map referenceMap = new HashMap();
		referenceMap.put("deptInfoOneDepthCategory",departmentService.getDepartmentIdNameList(param));	//상위부서정보를 가져와서 Map에 담는다.
		return referenceMap;
	}
        ...
}

부서 정보 수정페이지를 열었을때 참조 데이터로 가져온 상위 부서 정보 리스트 중에 해당 부서의 상위 부서값이 셀렉트 박스에서 기본으로 선택(“selected”)되서 보여져야 한다면, 참조데이터 중에 커맨드 객체와 일치하는 값을 일일히 조건문을 사용해서 비교하는 로직을 넣어주는 중노동을 해야 하나, 스프링 폼태그를 사용하면 간단하게 해결된다. referenceData 메소드가 반환한 상위 부서 정보 맵 데이터중에 해당 부서의 상위부서와 일치하는게 있으면 <form:options>은 해당 옵션을 선택(“selected”)으로 프린트한다.

<form:select path="superdeptid">
	<option value="">상위부서를 선택하세요.</option>
	<form:options items="${deptInfoOneDepthCategory}" />
</form:select>
onSubmit 메소드
protected ModelAndView onSubmit(Object command) throws Exception
protected ModelAndView onSubmit(Object command, BindException errors) throws Exception
protected ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) throws Exception

onSubmit() 메소드를 오버라이드한 메소드를 만들어서 폼의 내용을 전송을 하는 로직을 넣어보자. 커맨드 객체인 부서 정보 객체(Department)의 내용을 DB에 반영하고, 처리가 성공했으면 successView로 이동하고 실패하면 showForm 메소드를 호출한다. 이 showForm 메소드는 폼페이지를 다시 보여주기 위해 필요한 데이터와 에러정보를 ModelAndView에 넣고 formView로 이동한다.

package com.easycompany.controller.hierarchy;
...
public class UpdateDepartmentController extends SimpleFormController{
 
	private DepartmentService departmentService;
 
	public void setDepartmentService(DepartmentService departmentService){
		this.departmentService = departmentService;
	}
	...
	@Override
	protected ModelAndView onSubmit(HttpServletRequest request,
			HttpServletResponse response, Object command, BindException errors) throws Exception{
 
		Department department = (Department) command;
 
		try {
			departmentService.updateDepartment(department);
		} catch (Exception ex) {
			return showForm(request, response, errors);
		}
 
		return new ModelAndView(getSuccessView(), "department", department);
	}
	...
}

MultiActionController

지금까지 살펴본 Controller들은 하나의 액션에 하나의 Controller를 만드는 방식이다. (SimpleFormController가 하나의 url에 대해 GET, POST 메소드에 따라 분기 처리를 하기는 하지만.) 연관있는 여러 액션들을 하나의 Controller에 모으고 싶다면 MultiActionController를 상속받아 Controller를 작성하면 되는데 하나의 액션을 하나의 메소드로 작성한다. 그 메소드는 아래의 형식을 갖는다.

public (ModelAndView | Map | String | void) actionName(HttpServletRequest request, HttpServletResponse response);

그러면 어떤 액션(url)을 어떤 메소드가 처리 할지를 결정하는 일이 필요한데, 이때 도움을 주는 인터페이스가 MethodNameResolver이다. MethodNameResolver은 요청에 대해서 어떤 메소드가 처리 할지 메소드 이름을 반환하는데, 스프링은 다음과 같은 3가지 MethodNameResolver 구현 클래스를 제공한다.

  • ParameterMethodNameResolver : 특정 파라미터에 메소드 이름을 준다. 기본 파라미터는 action 이다.
  • InternalPathMethodNameResolver : URL 경로의 마지막 부분중에 확장자를 제외한 부분이 메소드 이름이 된다.
  • PropertiesMethodNameResolver : mapping 프로퍼티에 URL과 메소드 이름을 설정한다.

예제

상위 부서 리스트를 가져오는 액션과 특정 상위 부서에 속한 하위 부서 리스트를 가져 오는 액션을 처리하는 DepartmentListController를 작성해 보자.

package com.easycompany.controller.hierarchy;
...
public class DepartmentListController extends MultiActionController {
 
	...
	//상위 부서 리스트를 가져 온다.
	public ModelAndView departmentList(HttpServletRequest request, HttpServletResponse response){
 
		String depth = request.getParameter("depth");
 
		Map paramMap = new HashMap();
		paramMap.put("depth", depth);
 
		List<Department> departmentlist = departmentService.getDepartmentList(paramMap);
 
		ModelAndView mav = new ModelAndView("departmentlist");
		mav.addObject("departmentlist", departmentlist);
		return mav;
	}
 
	//특정 상위 부서에 속한 하위 부서 리스트를 가져 온다.	
	public ModelAndView subDepartmentList(HttpServletRequest request, HttpServletResponse response){
 
		String superdeptid = request.getParameter("superdeptid");
		String depth = request.getParameter("depth");
 
		Map paramMap = new HashMap();
		paramMap.put("depth", depth);
		paramMap.put("superdeptid", superdeptid);
 
		List<Department> departmentlist = departmentService.getDepartmentList(paramMap);
 
		ModelAndView mav = new ModelAndView("departmentsublist");
		mav.addObject("departmentlist", departmentlist);
		return mav;
	}
}
ParameterMethodNameResolver를 사용한 경우
<bean name="/departmentList.do" class="com.easycompany.controller.hierarchy.DepartmentListController"
	p:departmentService-ref="departmentService" 
	p:methodNameResolver-ref="paramResolver"/> <!-- ParameterMethodNameResolver를 MethodNameResolver로 사용-->
 
<bean id="paramResolver"
	class="org.springframework.web.servlet.mvc.multiaction.ParameterMethodNameResolver"
	p:paramName="method"/> <!--파라미터이름은 "method"-->

ParameterMethodNameResolver는 어떤 메소드를 호출할지에 대한 정보를 파라미터에 지정하는데, 파라미터 이름은 프로퍼티 “paramName”의 값이다.

  • /easycompany/departmentList.do?depth=1&method=departmentList → departmentList()
  • /easycompany/departmentList.do?superdeptid=1000&depth=2&method=subDepartmentList → subDepartmentList()
InternalPathMethodNameResolver를 사용한 경우
<bean id="departmentController" class="com.easycompany.controller.hierarchy.DepartmentListController"
	p:departmentService-ref="departmentService" 
	p:methodNameResolver-ref="pathResolver"/> <!-- InternalPathMethodNameResolver를 MethodNameResolver로 사용-->
 
<bean id="pathResolver"
	class="org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver"/>

InternalPathMethodNameResolver를 사용하면 URL /abc/foo.do로 요청이 들어올때 public ModelAndView foo(HttpServletRequest, HttpServletResponse) 메소드로 맵핑된다. 예제에서 보면, URL과 메소드간에 맵핑이 아래와 같이 이루어 진다.

  • /easycompany/departmentList.do?depth=1 → departmentList()
  • /easycompany/subDepartmentList.do?superdeptid=1000&depth=2 → subDepartmentList()
PropertiesMethodNameResolver를 사용한 경우
<bean id="departmentController" class="com.easycompany.controller.hierarchy.DepartmentListController"
	p:departmentService-ref="departmentService" 
	p:methodNameResolver-ref="propResolver"/> <!-- InternalPathMethodNameResolver를 MethodNameResolver로 사용-->
 
<bean id="propResolver"
	class="org.springframework.web.servlet.mvc.multiaction.PropertiesMethodNameResolver">
        <property name="mappings">
            <props>
                <prop key="/departmentList.do">departmentList</prop>
                <prop key="/subDepartmentList.do">subDepartmentList</prop>
            </props>
        </property>
</bean>

PropertiesMethodNameResolver는 URL과 메소드의 매핑 관계를 “mappings” 프로퍼티에 명시해준다.

UrlFilenameViewController

UrlFilenameViewController는 Controller에서 처리 로직이 없이 바로 view로 이동하는 경우에 사용하는 Controller이다. DispatcherServlet을 거쳐야 하지만, html 위주의 static한 페이지를 보여줄때 사용한다. 아래와 같이 URL path가 곧 뷰이름이 되며, prefix와 suffix를 지정할수도 있다.

"/index" -> "index" 
"/index.html" -> "index" 
"/index.html" + prefix "pre_" and suffix "_suf" -> "pre_index_suf" 
"/products/view.html" -> "products/view"

Controller를 따로 만들 필요가 없으며, 아래와 같이 빈 설정 파일에서 url과 UrlFilenameViewController를 매핑만 해주면 된다.

<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
        <props>
            <prop key="/login.do">urlFilenameViewController</prop>
            <prop key="/validator.do">urlFilenameViewController</prop>
        </props>
    </property>
</bean>
<bean id="urlFilenameViewController" class="org.springframework.web.servlet.mvc.UrlFilenameViewController" />

InternalResourceViewResolver가 아래와 같이 선언되어 있다면,

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
    p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" />

URL http://localhost:8080/easycompany/login.do로 요청이 들어올때, /easycompany/webapp/WEB-INF/jsp/login.jsp을 찾아서 보여준다.

참고자료

  • The Spring Framework - Reference Documentation 2.5.6
  • Spring Framework API Documentation 2.5.6

3.7 - Annotation 기반 Controller

스프링 프레임워크는 2.5 버전부터 Java 5+ 이상에서 @Controller를 이용한 Annotation 기반 컨트롤러 개발을 지원한다. 이는 기존의 계층형 Controller(SimpleFormController, MultiActionController)와 달리 인터페이스 구현 없이 더 유연하고 간결한 방식으로 요청을 처리할 수 있는 점이 주요 개선점이다.

Annotation-based Controller

개요

스프링 프레임워크는 2.5 버젼 부터 Java 5+ 이상이면 @Controller(Annotation-based Controller)를 개발할 수 있는 환경을 제공한다. 인터페이스 Controller를 구현한 SimpleFormController, MultiActionController 같은 기존의 계층형(Hierarchy) Controller와의 주요 차이점 및 개선점은 아래와 같다.

  • 어노테이션을 이용한 설정
    • XML 기반으로 설정하던 정보들을 어노테이션을 사용해서 정의한다.
  • 유연해진 메소드 시그니쳐
    • Controller 메소드의 파라미터와 리턴 타입을 좀 더 다양하게 필요에 따라 선택할 수 있다.
  • POJO-Style의 Controller
    • Controller 개발시에 특정 인터페이스를 구현 하거나 특정 클래스를 상속해야할 필요가 없다.
    • 하지만 폼 처리, 다중 액션등 기존의 계층형 Controller가 제공하던 기능들을 여전히 쉽게 구현할 수 있다.

계층형 Controller로 작성된 폼 처리를 @Controller로 구현하는 예도 설명한다. 예제 코드 easycompany의 Controller는 동일한 기능(또한 공통의 Service, DAO, JSP를 사용)을 계층형 Controller와 @Controller로 각각 작성했다.

  • 계층형 Controller - 패키지 com.easycompany.controller.hierarchy
  • @Controller - 패키지 com.easycompany.controller.annotation

설명

어노테이션을 이용한 설정

계층형 Controller들을 사용하면 여러 정보들(요청과 Controller의 매핑 설정 등)을 XML 설정 파일에 명시 해줘야 하는데, 복잡할 뿐 아니라 설정 파일과 코드 사이를 빈번히 이동 해야하는 부담과 번거로움이 될 수 있다. @MVC는 Controller 코드안에 어노테이션으로 설정함으로써 좀 더 편리하게 MVC 프로그래밍을 할 수 있도록 했다. @MVC에서 사용하는 주요 어노테이션은 아래와 같다.

이름설명
@Controller해당 클래스가 Controller임을 나타내기 위한 어노테이션
@RequestMapping요청에 대해 어떤 Controller, 어떤 메소드가 처리할지를 맵핑하기 위한 어노테이션
@RequestParamController 메소드의 파라미터와 웹요청 파라미터와 맵핑하기 위한 어노테이션
@ModelAttributeController 메소드의 파라미터나 리턴값을 Model 객체와 바인딩하기 위한 어노테이션
@SessionAttributesModel 객체를 세션에 저장하고 사용하기 위한 어노테이션
@RequestPartMultipart 요청의 경우, 웹요청 파라미터와 맵핑가능한 어노테이션(egov 3.0, Spring 3.1.x부터 추가)
@CommandMapController메소드의 파라미터를 Map형태로 받을 때 웹요청 파라미터와 맵핑하기 위한 어노테이션(egov 3.0부터 추가)
@ControllerAdviceController를 보조하는 어노테이션으로 Controller에서 쓰이는 공통기능들을 모듈화하여 전역으로 쓰기 위한 어노테이션(egov 3.0, Spring 3.2.X부터 추가)

@Controller

@MVC에서 Controller를 만들기 위해서는 작성한 클래스에 @Controller를 붙여주면 된다. 특정 클래스를 구현하거나 상속할 필요가 없다.

package com.easycompany.controller.annotation;
 
@Controller
public class LoginController {
   ...
}

앞서 DefaultAnnotationHandlerMapping에서 언급한 대로 <context:component-scan> 태그를 이용해 @Controller들이 있는 패키지를 선언해 주면 된다. @Controller만 스캔 한다면 include, exclude 등의 필터를 사용하라.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
				http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
 
        <context:component-scan base-package="com.easycompany.controller.annotation" />
 
</beans>

@RequestMapping

@RequestMapping은 요청에 대해 어떤 Controller, 어떤 메소드가 처리할지를 맵핑하기 위한 어노테이션이다. @RequestMapping이 사용하는 속성은 아래와 같다.

이름타입설명
valueString[]URL 값으로 맵핑 조건을 부여한다.
@RequestMapping(value=”/hello.do”) 또는 @RequestMapping(value={”/hello.do”, ”/world.do” })와 같이 표기하며,
기본값이기 때문에 @RequestMapping(”/hello.do”)으로 표기할 수도 있다.
”/myPath/*.do”와 같이 Ant-Style의 패턴매칭을 이용할 수도 있다.
Spring 3.1부터 URL뒤에 중괄호를 이용하여 변수값을 직접 받을 수 있도록 하였다. 아래 설명(URI Template Variable Enhancements)을 참고하라
methodRequestMethod[]HTTP Request 메소드값을 맵핑 조건으로 부여한다.
HTTP 요청 메소드값이 일치해야 맵핑이 이루어 지게 한다.
@RequestMapping(method = RequestMethod.POST)같은 형식으로 표기한다.
사용 가능한 메소드는 GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE이다
paramsString[]HTTP Request 파라미터를 맵핑 조건으로 부여한다.
params=“myParam=myValue”이면 HTTP Request URL중에 myParam이라는 파라미터가 있어야 하고 값은 myValue이어야 맵핑한다.
params=“myParam”와 같이 파라미터 이름만으로 조건을 부여할 수도 있고, ”!myParam”하면 myParam이라는 파라미터가 없는 요청 만을 맵핑한다.
@RequestMapping(params={“myParam1=myValue”, “myParam2”, ”!myParam3”})와 같이 조건을 주었다면,
HTTP Request에는 파라미터 myParam1이 myValue값을 가지고 있고, myParam2 파라미터가 있어야 하고, myParam3라는 파라미터는 없어야 한다.
consumesString[]설정과 Content-Type request헤더가 일치 할 경우에만 URL이 호출된다.
producesString[]설정과 Accept request헤더가 일치 할 경우에만 URL이 호출된다.

@RequestMapping은 클래스 단위(type level)나 메소드 단위(method level)로 설정할 수 있다.

type level

/hello.do 요청이 오면 HelloController의 hello 메소드가 수행된다.

@Controller
@RequestMapping("/hello.do")
public class HelloController {
 
    @RequestMapping   //type level에서 URL을 정의하고 Controller에 메소드가 하나만 있어도 요청 처리를 담당할 메소드 위에 @RequestMapping 표기를 해야 제대로 맵핑이 된다.
    public String hello(){
	...		
    }
}

method level

/hello.do 요청이 오면 hello 메소드, /helloForm.do 요청은 GET 방식이면 helloGet 메소드, POST 방식이면 helloPost 메소드가 수행된다.

@Controller
public class HelloController {	
 
	@RequestMapping(value="/hello.do")
	public String hello(){
		...
	}
 
	@RequestMapping(value="/helloForm.do", method = RequestMethod.GET)
	public String helloGet(){
		...
	}
 
	@RequestMapping(value="/helloForm.do", method = RequestMethod.POST)
	public String helloPost(){
		...
	}	
}

type + method level 둘 다 설정할 수도 있는데, 이 경우엔 type level에 설정한 @RequestMapping의 value(URL)를 method level에서 재정의 할수 없다.

/hello.do 요청시에 GET 방식이면 helloGet 메소드, POST 방식이면 helloPost 메소드가 수행된다.

@Controller
@RequestMapping("/hello.do")
public class HelloController {
 
	@RequestMapping(method = RequestMethod.GET)
	public String helloGet(){
		...
	}
 
	@RequestMapping(method = RequestMethod.POST)
	public String helloPost(){
		...
	}
}

AbstractController 상속받아 구현한 예제 코드 LoginController를 어노테이션 기반의 Controller로 구현해 보겠다.

기존의 LoginController는 URL /loginProcess.do로 오는 요청의 HTTP 메소드가 POST일때 handleRequestInternal 메소드가 실행되는 Controller였는데,

다음과 같이 구현할 수 있겠다.

package com.easycompany.controller.annotation;
...
@Controller
public class LoginController {
 
	@Autowired
	private LoginService loginService;
 
	@RequestMapping(value = "/loginProcess.do", method = RequestMethod.POST)
	public String login(HttpServletRequest request) {
 
		String id = request.getParameter("id");
		String password = request.getParameter("password");
 
		Account account = (Account) loginService.authenticate(id,password);
 
		if (account != null) {
			request.getSession().setAttribute("UserAccount", account);
			return "redirect:/employeeList.do";
		} else {
			return "login";
		}
	}	
}

위 예제 코드에서 서비스 클래스를 호출하기 위해서 @Autowired가 사용되었는데 자세한 내용은 여기를 참고하라.

type + method level + request

앞의 내용에서 추가되어 request의 header설정 일치 여부에 따라 URL호출이 가능하다.

다음 예제에서 URL이 /pets로 요청된 경우, POST타입의 request의 content-type이 application/json인 경우에만 다음 메소드가 호출된다.

@Controller
...
@RequestMapping(value = "/pets", method = RequestMethod.POST, consumes="application/json")
public void addPet(@RequestBody Pet pet, Model model) {
    // implementation omitted
}

다음 예제에서 GET타입으로 URL이 /pets/*로 요청된 경우, request의 accept header가 application/json인 경우에만 다음 메소드가 호출된다.

@Controller
...
@RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET, produces="application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId, Model model) 
    // implementation omitted
}

URI Template Variale Enhancements

@RequestMapping의 value에 URL뒤에 중괄호로 Controller메소드의 파라미터로 받을 값의 변수명을 입력해주면 변수를 받을 수 있다.

다음 예제에서 Controller메소드의 URL을 ”/user/view/{id}“로 설정하였을 때, 만약 /user/view/12345 로 URL요청이 들어오면 view함수의 파라미터인 id가 12345로 설정된다.

@RequestMapping("/user/view/{id}")
public String view(@PathVariable("id") int id) {
    // implementation omitted
}

@RequestParam

@RequestParam은 Controller 메소드의 파라미터와 웹요청 파라미터와 맵핑하기 위한 어노테이션이다. 관련 속성은 아래와 같다.

이름타입설명
valueString파라미터 이름
requiredboolean해당 파라미터가 반드시 필수 인지 여부. 기본값은 true이다.

아래 코드와 같은 방법으로 사용되는데, 해당 파라미터가 Request 객체 안에 없을때 그냥 null값을 바인드 하고 싶다면, pageNo 파라미터 처럼 required=false로 명시해야 한다.

name 파라미터는 required가 true이므로, 만일 name 파라미터가 null이면 org.springframework.web.bind.MissingServletRequestParameterException이 발생한다.

@Controller
public class HelloController {
 
    @RequestMapping("/hello.do")
    public String hello(@RequestParam("name") String name, //required 조건이 없으면 기본값은 true, 즉 필수 파라미터 이다. 파라미터 name이 존재하지 않으면 Exception 발생.
			@RequestParam(value="pageNo", required=false) String pageNo){ //파라미터 pageNo가 존재하지 않으면 String pageNo는 null.
	...		
    }
}

위에서 작성한 LoginController의 login 메소드를 보면 파라미터 아이디와 패스워드를 Http Request 객체에서 getParameter 메소드를 이용해 구하는데,

@RequestParam을 사용하면 아래와 같이 변경할수 있다.

package com.easycompany.controller.annotation;
...
@Controller
public class LoginController {
 
	@Autowired
	private LoginService loginService;
 
	@RequestMapping(value = "/loginProcess.do", method = RequestMethod.POST)
	public String login(
			HttpServletRequest request,
			@RequestParam("id") String id,
			@RequestParam("password") String password) {		
 
		Account account = (Account) loginService.authenticate(id,password);
 
		if (account != null) {
			request.getSession().setAttribute("UserAccount", account);
			return "redirect:/employeeList.do";
		} else {
			return "login";
		}
	}
}

@ModelAttribute

@ModelAttribute의 속성은 아래와 같다.

이름타입설명
valueString바인드하려는 Model 속성 이름.

@ModelAttribute는 실제적으로 ModelMap.addAttribute와 같은 기능을 발휘하는데, Controller에서 2가지 방법으로 사용된다.

1.메소드 리턴 데이터와 Model 속성(attribute)의 바인딩

메소드에서 비지니스 로직(DB 처리같은)을 처리한 후 결과 데이터를 ModelMap 객체에 저장하는 로직은 일반적으로 자주 발생한다.

...
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
	public String formBackingObject(@RequestParam("deptid") String deptid, ModelMap model) {
		Department department = departmentService.getDepartmentInfoById(deptid); //DB에서 부서정보 데이터를 가져온다.
		model.addAttribute("department", department); //데이터를 모델 객체에 저장한다.
		return "modifydepartment";
	}
...

@ModelAttribute를 메소드에 선언하면 해당 메소드의 리턴 데이터가 ModelMap 객체에 저장된다. 위 코드를 아래와 같이 변경할수 있는데, 사용자로 부터 GET방식의 /updateDepartment.do 호출이 들어오면, formBackingObject 메소드가 실행 되기 전에 DefaultAnnotationHandlerMapping이 org.springframework.web.bind.annotation.support.HandlerMethodInvoker을 이용해서 (@ModelAttribute가 선언된)getEmployeeInfo를 실행하고, 결과를 ModelMap객체에 저장한다. 결과적으로 getEmployeeInfo 메소드는 ModelMap.addAttribute(“department”, departmentService.getDepartmentInfoById(…)) 작업을 하게 되는것이다.

...
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
	public String formBackingObject() {
		return "modifydepartment";
	}
 
	@ModelAttribute("department")
	public Department getEmployeeInfo(@RequestParam("deptid") String deptid){
		return departmentService.getDepartmentInfoById(deptid); //DB에서 부서정보 데이터를 가져온다.
	}
	또는
	public @ModelAttribute("department") Department getDepartmentInfoById(@RequestParam("deptid") String deptid){
		return departmentService.getDepartmentInfoById(deptid);
	}
...

2.메소드 파라미터와 Model 속성(attribute)의 바인딩

@ModelAttribute는 ModelMap 객체의 특정 속성(attribute) 메소드의 파라미터와 바인딩 할때도 사용될수 있다. 아래와 같이 메소드의 파라미터에 ”@ModelAttribute(“department”) Department department” 선언하면 department에는 (Department)ModelMap.get(“department”) 값이 바인딩된다. 따라서, 아래와 같은 코드라면 formBackingObject 메소드 파라미터 department에는 getDepartmentInfo 메소드가 ModelMap 객체에 저장한 Department 데이터가 들어 있다.

...
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
	public String formBackingObject(@ModelAttribute("department") Department department) { //department에는 getDepartmentInfo에서 구해온 데이터들이 들어가 있다.
		System.out.println(employee.getEmployeeid());
		System.out.println(employee.getName());
		return "modifydepartment";
	}
 
	@ModelAttribute("department")
	public Department getDepartmentInfo(@RequestParam("deptid") String deptid){
		return departmentService.getDepartmentInfoById(deptid); //DB에서 부서정보 데이터를 가져온다.
	}
...

@SessionAttributes

@SessionAttributes는 model attribute를 session에 저장, 유지할 때 사용하는 어노테이션이다. @SessionAttributes는 클래스 레벨(type level)에서 선언할 수 있다. 관련 속성은 아래와 같다.

이름타입설명
typesClass[]session에 저장하려는 model attribute의 타입
valueString[]session에 저장하려는 model attribute의 이름

@RequestParam

Multipart request의 경우, 넘겨받은 Contents의 Content-Type에 따라 HttpMessageConverter를 통해 해당 타입대로 multipart컨텐츠를 얻을 때 사용하는 어노테이션이다.

예를 들어 다음과 같이 요청이 multipart로 들어올 때

POST /someUrl
Content-Type: multipart/mixed
 
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit
 
{
  "name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...

Controller에서 요청값은 아래와 같이 받을 수 있다.

@RequestMapping(value="/someUrl", method = RequestMethod.POST)
public String onSubmit(@RequestPart("meta-data") MetaData metadata,
                       @RequestPart("file-data") MultipartFile file) {
    // ...
}

@CommandMap

@CommandMap은 실행환경 3.0환경부터 추가된 Controller에서 Map형태로 웹요청 값을 받았을 때 다른 Map형태의 argument와 구분해주기 위한 어노테이션이다. @CommandMap은 파라미터 레벨(type level)에서만 선언할 수 있다.

사용 방법은 다음과 같다.

   @RequestMapping("/test.do")
   public void test(HttpServletRequest request, @CommandMap Map<String, String> commandMap) {
	//생략
   }

자세한 사용방법은 AnnotationCommandMapArgumentResolver을 참고한다.

@ControllerAdvice

@ControllerAdvice 어노테이션을 통해 Controller에서 쓰이는 몇가지 어노테이션 기능들을 모듈화하여 전역으로 쓸 수 있다. @ControllerAdvice은 @RequestMapping이 붙은 메소드를 지원하며 다음과 같은 Controller 어노테이션을 지원한다.

이름설명
@ExceptionHandler@ExceptionHandler 뒤에 붙은 Exception이 발생했을 때, 전역적으로 예외처리가 가능하다.
@InitBinder모델 검증과 바인딩을 하기 위한 Annotation으로써 JSR-303 빈 검증기능을 사용하는 스프링 validator를 사용할 수 있다.
@ModelAttribute도메인 오브젝트나 DTO프로퍼티에 요청파라미터를 한 번에 받을 수 있는 @ModelAttribute를 전역으로 사용 가능하다.

@ExceptionHandler with @ControllerAdvice

기존에는 예외발생시, AnnotationMethodHandlerExceptionResolver가 Controller내부에서 @ExceptionHandler가 붙은 메소드를 찾아 예외처리를 해준다.

Controller 내부에서만 @ExceptionHandler가 동작하기 때문에 각 Controller별로 @ExceptionHandler 메소드를 만들어야했다.

@Controller
public class HelloController {
 
  @RequestMapping("/hello")
  public void hello() {    
    //DataAccessException이 일어날 가능성
  }
     
  // Controller 내부에서 DataAccessException발생시 호출
  @ExceptionHandler(DataAccessException.class)
  public ModelAndView dataAccessExceptionHandler(DataAccessException e) {
    return new ModelAndView("dataexception").addObject("msg", ex.getMessage();
  }
}

Spring 3.2부터는 @ControllerAdvice를 이용하여 @ExceptionHandler를 전역으로 쓸 수 있다.

@ControllerAdvice + @ExceptionHandler를 통해 각각의 Exception에 대하여 전역적인 후처리 관리가 가능해진다.

즉, Controller마다 @ExceptionHandler를 만들지 않더라도 @ControllerAdvice가 붙은 Class안에서 여러 Exception에 대한 처리가 가능한 @ExceptionHandler 메소드를 만들면 로지컬한 Exception별 후처리가 가능해지는 것이다.

@ControllerAdvice와 함께 @ExceptionHandler를 쓰는 방법은 다음과 같다. 다음과 같이 쓰는 경우, Controller에서 발생하는 해당 Exception들이 예외처리가 된다.

@ControllerAdvice
public class CentralControllerHandler {
 
    @ExceptionHandler({EgovBizException.class})
    public ModelAndView handleEgovBizException(EgovBizException ee) {
    //생략
    }
    
    @ExceptionHandler({BaseException.class})
    public ModelAndView handleBaseException(BaseException be) {
    //생략
    }
}

@InitBinder with @ControllerAdvice

@ControllerAdvice가 붙은 Class내부에서 @InitBinder 메소드를 씀으로써 이를 전역으로 쓸 수도 있다.

  • @InitBinder
    • WebDataBinder를 초기화하는 메소드를 지정할 수 있는 설정을 제공한다. WebDataBinder는 Web request parameter를 javaBean객체에 바인딩하는 특정한 DataBinder이다.
    • 일반적으로 annotation handler메소드의 command와 form객체 인자를 조사하는데 사용된다.(CustomEditor를 등록하거나 Validator를 등록할 때 쓰인다)

@ControllerAdvice 와 함께 @InitBinder를 쓰는 방법은 다음과 같다. @InitBinder는 Controller에서 @Valid를 쓰는 경우에만 해당 파라미터의 데이터 검증이 적용된다.

Person객체를 모델바인딩 및 검증해주는 PersonValidator를 이용하는 경우이다.

@ControllerAdvice
public class CentralControllerHandler {
 
    @InitBinder
    public void initBinder(WebDataBinder binder) {
    binder.setValidator(new PersonValidator());
    }
 
    //생략
}

이 때 Controller의 구현 예이다.

@RequestMapping(value="/persons")
public void updatePerson(@Valid Person person, HttpServletRequest request, HttpServletResponse response) {
    personService.updatePerson(person);
    //생략
}

@ModelAttribute와 @ControllerAdvice

@ControllerAdvice를 통해 @ModelAttribute의 메소드 또한 전역으로 쓰일 수 있다.

@ControllerAdvice
public class GlobalControllerAdvice {
 
  ... 
 
  @ModelAttribute("model")
  public Model getModel() {
    return this.model();
  }
}

유연해진 메소드 시그니쳐

@RequestMapping을 적용한 Controller의 메소드는 아래와 같은 메소드 파라미터와 리턴 타입을 사용할수 있다.

특정 클래스를 확장하거나 인터페이스를 구현해야 하는 제약이 없기 때문에 계층형 Controller 비해 유연한 메소드 시그니쳐를 갖는다.

@Controller의 메소드 파라미터

사용가능한 메소드 파라미터는 아래와 같다.

  • Servlet API
    • ServletRequest, HttpServletRequest, HttpServletResponse, HttpSession 같은 요청,응답,세션관련 Servlet API들
  • WebRequest, NativeWebRequest
    • org.springframework.web.context.request.WebRequest, org.springframework.web.context.request.NativeWebRequest
  • java.util.Locale
  • java.io.InputStream / java.io.Reader
  • java.io.OutputStream / java.io.Writer
  • @RequestParam
    • HTTP Request의 파라미터와 메소드의 argument를 바인딩하기 위해 사용하는 어노테이션
  • java.util.Map / org.springframework.ui.Model / org.springframework.ui.ModelMap
    • 뷰에 전달할 모델데이터들
  • Command/form 객체
    • HTTP Request로 전달된 parameter를 바인딩한 커맨드 객체, @ModelAttribute을 사용하면 alias를 줄수 있다.
  • Errors, BindingResult
    • org.springframework.validation.Errors / org.springframework.validation.BindingResult 유효성 검사후 결과 데이터를 저장한 객체
  • SessionStatus
    • org.springframework.web.bind.support.SessionStatus 세션폼 처리시에 해당 세션을 제거하기 위해 사용된다.

메소드는 임의의 순서대로 파라미터를 사용할수 있다. 단, BindingResult가 메소드의 argument로 사용될 때는 바인딩 할 커맨드 객체가 바로 앞에 와야 한다.

public String updateEmployee(...,@ModelAttribute("employee") Employee employee,			
			BindingResult bindingResult,...) /* (O) */
 
public String updateEmployee(...,BindingResult bindingResult,
                        @ModelAttribute("employee") Employee employee,...) /* (X) */

이 외의 타입을 메소드 파라미터로 사용하려면?

스프링 프레임워크는 위에서 언급한 타입이 아닌 custom arguments도 메소드 파라미터로 사용할 수 있도록 org.springframework.web.bind.support.WebArgumentResolver라는 인터페이스를 제공한다.

WebArgumentResolver를 사용한 예제는 이곳을 참고

@Controller의 메소드 리턴 타입

사용가능한 메소드 리턴 타입은 아래와 같다.

  • ModelAndView
    • 커맨드 객체와 @ModelAttribute이 적용된 메소드의 리턴 데이터가 담긴 Model 객체와 View 정보가 담겨 있다.
        @RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
	public ModelAndView formBackingObject(@RequestParam("deptid") String deptid) {
		Department department = departmentService.getDepartmentInfoById(deptid);
		ModelAndView mav = new ModelAndView("modifydepartment");
		mav.addObject("department", department);
		return mav;
	}

또는

	public ModelAndView formBackingObject(@RequestParam("deptid") String deptid, ModelMap model) {
		Department department = departmentService.getDepartmentInfoById(deptid);
		model.addAttribute("department", department);
		ModelAndView mav = new ModelAndView("modifydepartment");
		mav.addAllObjects(model);
		return mav;
	}
  • Model (또는 ModelMap)
    • 커맨드 객체와 @ModelAttribute이 적용된 메소드의 리턴 데이터가 Model 객체에 담겨 있다.
    • View 이름은 RequestToViewNameTranslator가 URL을 이용하여 결정한다.
    • 인터페이스 RequestToViewNameTranslator의 구현클래스인 DefaultRequestToViewNameTranslator가 View 이름을 결정하는 방식은 아래와 같다.
http://localhost:8080/gamecast/display.html -> display
http://localhost:8080/gamecast/displayShoppingCart.html -> displayShoppingCart
http://localhost:8080/gamecast/admin/index.html -> admin/index
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
	public Model formBackingObject(@RequestParam("deptid") String deptid, Model model) {
		Department department = departmentService.getDepartmentInfoById(deptid);
		model.addAttribute("department", department);
		return model;
	}

또는

	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
	public Model formBackingObject(@RequestParam("deptid") String deptid) {
		Department department = departmentService.getDepartmentInfoById(deptid);
		Model model = new ExtendedModelMap();
		model.addAttribute("department", department);
		return model;
	}
  • Map
    • 커맨드 객체와 @ModelAttribute이 적용된 메소드의 리턴 데이터가 Map 객체에 담겨 있으며, View 이름은 역시 RequestToViewNameTranslator가 결정한다.
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
	public Map formBackingObject(@RequestParam("deptid") String deptid) {
		Department department = departmentService.getDepartmentInfoById(deptid);
		Map model = new HashMap();
		model.put("department", department);
		return model;
	}

또는

	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
	public Map formBackingObject(@RequestParam("deptid") String deptid, Map model) {
		Department department = departmentService.getDepartmentInfoById(deptid);
		model.put("department", department);
		return model;
	}
  • String
    • 리턴하는 String 값이 곧 View 이름이된다.
    • 커맨드 객체와 @ModelAttribute이 적용된 메소드의 리턴 데이터가 Model(또는 ModelMap)에 담겨있다.
    • 리턴할 Model(또는 ModelMap)객체가 해당 메소드의 argument에 선언되어 있어야한다.
/* (O) */
@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
public String formBackingObject(@RequestParam("deptid") String deptid, ModelMap model) {
    Department department = departmentService.getDepartmentInfoById(deptid);
    model.addAttribute("department", department);
    return "modifydepartment";
}

/* (X) */
@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
public String formBackingObject(@RequestParam("deptid") String deptid) {
    Department department = departmentService.getDepartmentInfoById(deptid);
    ModelMap model = new ModelMap();
    model.addAttribute("department", department);
    return "modifydepartment";
}
  • View
    • View를 리턴한다. 커맨드 객체와 @ModelAttribute이 적용된 메소드의 리턴 데이터가 Model(또는 ModelMap)에 담겨 있다.
  • void
    • 메소드가 ServletResponse / HttpServletResponse등을 사용해서 직접 응답을 처리하는 경우. View 이름은 RequestToViewNameTranslator가 결정한다.

POJO-Style의 Controller

@MVC는 Controller 개발시에 특정 인터페이스를 구현 하거나 특정 클래스를 상속해야할 필요가 없다.

Controller의 메소드에서 Servlet API를 반드시 참조하지 않아도 되며, 훨씬 유연해진 메소드 시그니쳐로 개발이 가능하다.

여기서는 SimpleFormController의 폼 처리 액션을 @Controller로 구현함으로써, POJO-Style에 가까워졌지만 기존의 계층형 Controller에서 제공하던 기능들을 여전히 구현할 수 있음을 보이고자 한다.

FormController by SimpleFormController -> @Controller

앞서 SimpleFormController을 설명하면서 예제로 작성된 com.easycompany.controller.hierarchy.UpdateDepartmentController를 @ModelAttribute와 @RequestMapping을 이용해서 같은 기능을 @Controller로 작성해 보겠다.

JSP 소스는 동일한 것을 사용한다. 이곳의 예제 화면 이미지 및 JSP 코드를 참고. 기존의 UpdateDepartmentController를 보면 3가지 메소드로 이루어졌다.

  • referenceData
    • 입력폼에 필요한 참조데이터인 상위부서정보를 가져와서 Map 객체에 저장한다. 이후에 이 Map 객체는 스프링 내부 로직에 의해 ModelMap 객체에 저장된다.
  • formBackingObject
    • GET 방식 호출일때 초기 입력폼에 들어갈 부서 데이터를 리턴한다. 이 데이터 역시 ModelMap 객체에 저장된다.
  • onSubmit
    • POST 전송시에 호출되며 폼 전송을 처리한다.
package com.easycompany.controller.hierarchy;
...
 
public class UpdateDepartmentController extends SimpleFormController{
 
	private DepartmentService departmentService;
 
	public void setDepartmentService(DepartmentService departmentService){
		this.departmentService = departmentService;
	}
 
	//상위부서리스트(selectbox)는 부서정보클래스에 없으므로 , 상위부서리스트 데이터를 DB에서 구해서 별도의 참조데이터로 구성한다.
	@Override
	protected Map referenceData(HttpServletRequest request, Object command, Errors errors) throws Exception{
 
		Map referenceMap = new HashMap();
		referenceMap.put("deptInfoOneDepthCategory",departmentService.getDepartmentIdNameList("1"));	//상위부서정보를 가져와서 Map에 담는다.
		return referenceMap;
	}
 
	@Override
	protected Object formBackingObject(HttpServletRequest request) throws Exception {
		if(!isFormSubmission(request)){	// GET 요청이면
			String deptid = request.getParameter("deptid");
			Department department = departmentService.getDepartmentInfoById(deptid);//부서 아이디로 DB를 조회한 결과가 커맨드 객체 반영.
			return department;
		}else{	// POST 요청이면
			//AbstractFormController의 formBackingObject을 호출하면 요청객체의 파라미터와 설정된 커맨드 객체간에 기본적인 데이터 바인딩이 이루어 진다.
			return super.formBackingObject(request);
		}
	}
 
	@Override
	protected ModelAndView onSubmit(HttpServletRequest request,
			HttpServletResponse response, Object command, BindException errors) throws Exception{
 
		Department department = (Department) command;
 
		try {
			departmentService.updateDepartment(department);
		} catch (Exception ex) {
			return showForm(request, response, errors);
		}
 
		return new ModelAndView(getSuccessView(), "department", department);
	}
}

@Controller로 작성된 com.easycompany.controller.annotation.UpdateDepartmentController은 3개의 메소드로 이루어져 있다.

계층형 Controller인 기존의 UpdateDepartmentController와는 달리 각 메소드는 Override 할 필요없기 때문에 메소드 이름은 자유롭게 지을 수 있다.

쉬운 비교를 위해 SimpleFormController과 동일한 메소드 이름을 선택했다.

  • referenceData
    • 입력폼에 필요한 참조데이터인 상위부서정보를 가져와서 ModelMap에 저장한다.(by @ModelAttribute)
  • formBackingObject
    • GET 방식 호출일때 처리를 담당한다. 초기 입력폼 구성을 위한 부서데이터를 가져와서 ModelMap에 저장한다.
  • onSubmit
    • POST 전송시에 호출되며 폼 전송을 처리한다.

(POJO에 가까운) 프레임워크 코드들은 감춰졌고, 보다 직관적으로 비지니스 내용을 표현할 수 있게 되었다고 생각한다.

package com.easycompany.controller.annotation;
 
...
@Controller
public class UpdateDepartmentController {
 
	@Autowired
	private DepartmentService departmentService;
 
	//상위부서리스트(selectbox)는 부서정보클래스에 없으므로 , 상위부서리스트 데이터를 DB에서 구해서 별도의 참조데이터로 구성한다.
	@ModelAttribute("deptInfoOneDepthCategory")
	public Map<String, String> referenceData() {
		return departmentService.getDepartmentIdNameList("1");
	}
 
	// 해당 부서번호의 부서정보 데이터를 불러와 입력폼을 채운다
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.GET)
	public String formBackingObject(@RequestParam("deptid") String deptid, ModelMap model) {
		Department department = departmentService.getDepartmentInfoById(deptid);
		model.addAttribute("department", department); //form tag의 commandName은 이 attribute name과 일치해야 한다. <form:form commandName="department">.
		return "modifydepartment";
	}
 
	//사용자가 데이터 수정을 끝내고 저장 버튼을 누르면 수정 데이터로 저장을 담당하는 서비스(DB)를 호출한다.
	//저장이 성공하면 부서리스트 페이지로 이동하고 에러가 있으면 다시 입력폼페이지로 이동한다.
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.POST)
	public String onSubmit(@ModelAttribute("department") Department department, BindingResult bindingResult) {
 
		//validation code
		new DepartmentValidator().validate(department, bindingResult);		
		if(bindingResult.hasErrors()){
			return "modifydepartment";
		}
 
		try {
			departmentService.updateDepartment(department);
			return "redirect:/departmentList.do?depth=1";
		} catch (Exception e) {
			e.printStackTrace();
			return "modifydepartment";
		}
	}
}

참고자료

3.8 - Spring의 Validator 인터페이스와 유효성 검증

스프링 프레임워크는 Validator 인터페이스를 제공해 웹, 데이터 접근 등 다양한 계층의 객체에 유효성 검증을 지원한다. 또한 Jakarta Commons Validator와 같은 외부 Validator도 스프링에서 통합하여 사용할 수 있다.

Validation

개요

객체의 유효성 검증을 위해 스프링 프레임워크는 org.springframework.validation.Validator라는 인터페이스를 제공한다. Validator는 특정 계층에 종속적인 구조가 아니라서, web이나 data-access등 어떤 계층의 객체라도 유효성 검증이 가능하게 한다. Jakarta Commons Validator나 Valang 같은 외부 Validator들도 Spring 프레임워크에서 사용할 수 있다. Spring Modules를 이용한 Jakarta Commons Validator 사용 방법에 대해서는 Spring Framework에서 Commons Validator 사용 을 참고하라.

설명

부서 정보를 수정하는 페이지에서 커맨드 객체인 부서 정보 클래스를 유효성 검증하는 코드를 작성해 보자. 부서 클래스인 Department 클래스는 아래와 같다.

package com.easycompany.domain;
 
public class Department {
 
	private String deptid;   //부서아이디
	private String deptname;  //부서이름
	private String superdeptid;  //상위부서아이디
	private String superdeptname; //상위부서이름
	private String depth;  //부서레벨
	private String description;  //부서설명
 
	//위 프로퍼티들의 setter/getter
}

Validator 구현

인터페이스 org.springframework.validation.Validator의 메소드는 다음과 같다.

  • boolean supports(Class clazz) : 주어진 객체(clazz)에 대해 Validator가 지원 가능한가?
  • void validate(Object target, Errors errors) : 주어진 객체(target)에 대해서 유효성 체크를 수행하고, 유효성 에러 발생시 주어진 Errors객체에 관련 정보가 저장된다.

구현 Validator 클래스를 만들때는 위 두 메소드를 구현해야 한다.

Department를 유효성 검증 하기 위한 DepartmentValidator를 만들어 보자. Validation 조건은 부서이름(deptname) 프로퍼티는 반드시 값이 존재해야 하며, 부서설명(description) 프로퍼티는 입력값의 길이가 10 이상이어야 한다.

package com.easycompany.validator;
 
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import com.easycompany.domain.Department;
 
public class DepartmentValidator implements Validator {
 
	public boolean supports(Class clazz) {
		return Department.class.isAssignableFrom(clazz);
	}
 
	public void validate(Object target, Errors errors) {
 
		Department department = (Department)target;
 
		if (isEmptyOrWhitespace(department.getDeptname())) { //부서 이름 프로퍼티 값이 존재하는가? 
			errors.rejectValue("deptname", "required");
		}
 
		if (department.getDescription() == null || department.getDescription().length() < 10) { //부서설명 프로퍼티는 값의 길이가 10 이상인가?
			errors.rejectValue("description", "lengthsize", new Object[]{10}, "description's length must be larger than 10.");
		}		
	}	
 
	public boolean isEmptyOrWhitespace(String value){
		if (value == null || value.trim().length() == 0) {
			return true;
		} else {
			return false;
		}
	}
}

위 코드에서 처럼 유효성 검증이 실패한 경우 Errors 인터페이스의 rejectValue 메소드를 실행하는데, Errors 인터페이스에 대한 자세한 설명은 여기를 참고하라.

errors.rejectValue(“deptname”, “required”);

  • deptname 프로퍼티에 대해서 유효성 검증시 에러가 발생했고, 관련 메시지 key는 “required”란 의미이다.

errors.rejectValue(“description”, “lengthsize”, new Object[]{10}, “description’s length must be larger than 10.”);

  • description 프로퍼티에 대해서 유효성 검증시 에러가 발생했고, 관련 메시지 key는 “lengthsize” 이며 메시지에 전달될 argument는 10이며, 해당 메시지 key가 존재 하지 않으면 “description’s length must be larger than 10.”란 메시지를 사용한다는 의미이다.

스프링에서는 유효성 검증을 위한 ValidationUtils라는 유틸 클래스를 제공한다. 부서 이름 프로퍼티(deptname) 값이 null또는 white space인지 체크하는 부분은 ValidationUtils의 rejectIfEmptyOrWhitespace 메소드를 사용해서 작성할 수 있다.

package com.easycompany.validator;
 
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.validation.ValidationUtils;
import com.easycompany.domain.Department;
 
public class DepartmentValidator implements Validator {
 
	public boolean supports(Class clazz) {
		return Department.class.isAssignableFrom(clazz);
	}
 
	public void validate(Object target, Errors errors) {
 
		Department department = (Department)target;
 
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "deptname", "required");
 
		if (department.getDescription() == null || department.getDescription().length()<10) { //부서설명 프로퍼티는 입력값의 길이가 10 이상인가?
			errors.rejectValue("description", "lengthsize", new Object[]{10}, "description's length must be larger than 10.");
		}		
	}
}

에러 메시지 설정

message 프로퍼티 파일에서 메시지 key인 “required”, “lengthsize”에 대한 메시지 설정을 한다.

required=필수 입력값입니다.
lengthsize={0}자 이상 입력해야 합니다.

Controller에서 validation

Controller에서 validation을 수행하는 코드를 적용해 보자. 폼을 전송하는 순간에 유효성 검증을 하기 원한다면, com.easycompany.controller.annotation.UpdateDepartmentController 에서 onSubmit 메소드에 validation 코드를 추가한다.

package com.easycompany.controller.annotation;
...
import org.springframework.validation.BindingResult;
import com.easycompany.validator.DepartmentValidator;
 
@Controller
public class UpdateDepartmentController {
 
	//사용자가 데이터 수정을 끝내고 저장 버튼을 누르면 수정 데이터로 저장을 담당하는 서비스(DB)를 호출한다.
	//저장이 성공하면 부서리스트 페이지로 이동하고 에러가 있으면 다시 입력폼페이지로 이동한다.
	@RequestMapping(value = "/updateDepartment.do", method = RequestMethod.POST)
	public String onSubmit(@ModelAttribute("department") Department department, BindingResult bindingResult) {
 
		//validation code
		new DepartmentValidator().validate(department, bindingResult); //validation을 수행한다.
		if(bindingResult.hasErrors()){ //validation 에러가 있으면,
			return "modifydepartment";  //이 페이지로 이동.
		}
 
		try {
			departmentService.updateDepartment(department);
			return "redirect:/departmentList.do?depth=1";
		} catch (Exception e) {
			e.printStackTrace();
			return "modifydepartment";
		}
	}
}

JSP

손쉬운 에러 메시지 표기를 위해 Spring 폼태그 <form:errors/>를 사용할 것을 권장한다. /easycompany/webapp/jsp/annotation/modifydepartment.jsp

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
...
<form:form commandName="department">
<table>
...
	<tr>
		<th>부서이름</th>
		<td><form:input path="deptname" size="20"/><form:errors path="deptname" /></td>
	</tr>
...
	<tr>
		<th>설명</th>
		<td><form:textarea path="description" rows="10" cols="40"/><form:errors path="description" /></td>
	</tr>	
</table>
</form:form>
...

TEST

부서 이름값을 비우고, 부서설명 부분에 10자 이하로 입력한 후에 저장 버튼을 누르면, 다시 부서정보수정 페이지로 돌아와서 아래와 같이 에러 메시지가 출력될 것이다.

web-servlet-validation

참고자료

  • Spring Framework API Documentation 2.5.6

3.9 - Bean Validation (JSR-303)

JSR-303(Bean Validation) 스펙은 @Valid 애노테이션을 사용해 모델 객체 필드의 자동 검증을 지원한다. 이를 통해 표준화된 방식으로 애노테이션을 활용한 필드 검증이 가능하다.

Bean Validation (JSR-303)

개요

화면처리: validation을 통해 검증방법을 알아보았다. 이전과는 다르게 JSR-303(Bean Validation) 스펙은 자동 검증 방식을 제공한다. @javax.validation.Valid애노테이션을 사용하여 내부적으로(자동으로) 검증이 수행된다.

또한, 최근에 표준 스펙으로 인증받은 JSR-303 빈 검증방식을 이용하여 모델 오브젝트 필드에서 애노테이션을 이용해 검증을 진행할 수 있다.

설명

@Valid를 이용한 자동검증

기존의 검증 방식을 자동 검증 방식으로 변경하였으며, 방법은 컨트롤러 메소드의 @ModelAttribute 파라미터에 @Valid 애노테이션을 추가한다. 그러면 validate() 메소드를 실행하는 대신 바인딩 과정에서 자동으로 검증이 진행된다.

@Controller
public class ExampleController {
 
	@Autowired ExampleValidator validator;
 
	@InitBinder
	public void initBinder(WebDataBinder dataBinder){
		dataBinder.setValidator(this.validator);
	}
 
 
	@RequestMapping("/insertMember.do")
	public String insertMember(@ModelAttribute("memberVO") @Valid MemberVO memberVO, BindingResult bindingResult, ..) {
		//..
	}
}

JSR-303 빈 검증(bean validation) 기능

위의 방식은 기존의 검증방식을 자동 검증으로 변경한 방법이며 다음에 설명한 검증방법은 제약조건을 빈에 직접 설정하여 검증하는 방식이다.

먼저, 클래스패스에 의존 라이브러리를 추가해야 한다. 메이븐을 사용 중이라면 다음 의존 라이브러리를 프로젝트에 추가한다.

<dependency>
	<groupId>javax.validation</groupId>
	<artifactId>validation-api</artifactId>
	<version>1.0.0.GA</version>
</dependency>
<dependency>
	<groupId>org.hibernate</groupId>
	<artifactId>hibernate-validator</artifactId>
	<version>4.0.0.GA</version>
</dependency>

다음은 JSR-303 제약조건 애노테이션이 적용된 모델오브젝트 예제이다.

public class MemberVO{
 
	@NotNull
	@Size(min = 1, max = 50, message="이름을 입력하세요.")
	private String name;
 
	@Pattern(regexp=".+@.+\\.[a-z]+", message= "이메일 형식이 잘못되었습니다.")
	private String email;
 
	//..
}

@NotNull은 빈문자열을 검증하지 못하기 때문에 @Size(min=1)을 사용하여 빈 문자열을 확인해야 한다.

위와같은 제약조건 애노테이션을 사용해 검증을 수행하기 위해서는 LocalValidatiorFactoryBean을 빈으로 등록해 줘야 한다. LocalValidatiorFactoryBean은 JSR-303의 검증기능을 스프링의 Validator처럼 사용할 수 있게 해주는 일종의 어댑터다. LocalValidatiorFactoryBean을 빈으로 등록하면 컨트롤러에서 Validator타입으로 DI 받아서 @InitBinder에서 WebDataBinder에 설정하거나 코드에서 직접 Validator처럼 사용할 수 있다.

<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" />

빈 검증 기능을 validator로 사용하는 컨트롤러 예제이다.

@Controller
public class ExampleController {
 
	@Resource
	Validator validator;
 
	@InitBinder
	public void initBinder(WebDataBinder dataBinder){
		dataBinder.setValidator(this.validator);
	}
 
	@RequestMapping("/insertMember.do")
	public String insertMember(@ModelAttribute("memberVO") @Valid MemberVO memberVO, BindingResult bindingResult, ..) {
		//..
	}
}

web-servlet-declarative-validation

참고자료

3.10 - View

Controller는 요청을 처리한 후 View 이름과 데이터를 ModelAndView에 저장해 DispatcherServlet에 반환하고, DispatcherServlet은 ViewResolver를 통해 실제 View 객체를 얻는다. 이 View는 Model 객체의 정보를 출력하며, 스프링은 JSP에서 편리한 데이터 출력을 위해 Spring form tag library를 제공한다.

View

개요

Controller가 요청에 대한 처리를 하고, View 이름과 데이터(Model)를 ModelAndView에 저장해 DispatcherServlet에 반환(return)하면, DispatcherServlet은 View 이름을 가지고 ViewResolver에게서 실제 View 객체를 얻고, 이 View는 Controller가 저장한 Model 객체의 정보를 출력한다. 여기서는 View와 ViewResolver, 그리고 JSP에서 편리한 데이터 출력을 위해 스프링이 제공하는 Spring form tag library에 대해서 설명한다.

설명

ViewResolver

Controller는 코드내에서 실제 View 객체를 생성하지 않고 View 이름만을 결정할 수 있는데, 이로써 Controller와 View의 분리(decoupling)를 가능하게 한다.

package com.easycompany.controller.hierarchy;
...
public class EmployeeListController extends AbstractCommandController{
        ...
	@Override
	protected ModelAndView handle(HttpServletRequest request,
			HttpServletResponse response, Object command, BindException errors)
			throws Exception {
		...
		List<Employee> employeelist = employeeService.getAllEmployees(commandMap);
 
		ModelAndView modelview = new ModelAndView();
		modelview.addObject("employeelist", employeelist);
		...
		//직접 View 객체를 생성하지 않고,
		//View view = new InternalResourceView("/jsp/employeelist.jsp"); 
		//modelview.setView(view);
		//View 이름만을 저장.
		modelview.setViewName("employeelist");
 
		return modelview;
	}
}

이때, DispatcherServlet에 실제 View 객체를 구해주는건 Controller가 아니라 ViewResolver가 담당한다. ViewResolver는 Controller가 반환한 ModelAndView 객체에 담긴 View 이름을 가지고 실제 View 객체를 반환하는 인터페이스이다. Spring에서 제공하는 ViewResolver 구현 클래스는 아래와 같다.

ViewResolver설명
XmlViewResolverView이름과 View 클래스간의 매핑정보가 담긴 XML로 부터 View이름에 해당하는 View를 구한다.
기본설정 파일은 /WEB-INF/views.xml이다.
ResourceBundleViewResolverView이름과 View 클래스간의 매핑정보가 담긴 리소스 번들(프로퍼티파일)로 부터 View 이름에 해당하는 View를 구한다.
기본설정 파일은 views.properties이다.
InternalResourceViewResolver/UrlBasedViewResolver특정 디렉토리 경로의 JSP파일들을 호출할 때 편리하게 사용할수 있다.
기본적으로 사용하는 View 클래스는 InternalResourceView이며, View 이름이 곧 JSP 파일이름이 된다.
VelocityViewResolver /FreeMarkerViewResolverVelocity/FreeMarker 연동시에 사용한다.

InternalResourceViewResolver/UrlBasedViewResolver

비지니스 로직 처리가 끝난 후 ”/jsp/main/abc.jsp” 경로의 JSP 파일로 forwarding하는 Controller가 있다고 하면, InternalResourceViewResolver/UrlBasedViewResolver를 사용해서 아래와 같이 Controller를 작성하고, 빈 정의 파일에 설정할 수 있다.

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"/>

또는

<bean class="org.springframework.web.servlet.view.UrlBasedViewResolver "/>
@Controller
public class HelloController {
	@RequestMapping("...")
	public String hello(){
		... //비지니스 로직 처리.
		return "/jsp/main/abc.jsp"; //뷰이름이 곧 JSP 파일의 경로.
	}	
}

InternalResourceViewResolver/UrlBasedViewResolver의 프로퍼티 prefix, suffix를 사용하면 좀더 간단하게 처리할수 있는데, JSP가 특정 디렉토리 경로 아래에 있고, 예를 들어 /jsp/main 디렉토리 아래, 확장자는 .jsp 이라면,

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
	p:prefix="/jsp/main/" p:suffix=".jsp" />

또는

<bean class="org.springframework.web.servlet.view.UrlBasedViewResolver"
	p:prefix="/jsp/main/" p:suffix=".jsp" />
@Controller
public class HelloController {
	@RequestMapping("...")
	public String hello(){
		...
		return "abc"; //prefix와 suffix를 제외한 부분만 표기.
	}	
}

간단히 뷰이름을 설정할 수 있다.

View

Spring이 제공하는 View 클래스를 사용할 수도 있지만, UI Tool 등과의 연동등으로 인해 View 클래스를 직접 작성해야 하는 경우도 발생한다. 인터페이스 View를 직접 구현해서 View 클래스를 만들수도 있지만, AbstractView를 확장하여 구현해보자. renderMergedOutputModel 메소드를 구현하면 되는데, 아래와 같은 메소드 시그니쳐를 가지고 있다.

protected abstract void renderMergedOutputModel(Map model,
                                                HttpServletRequest request,
                                                HttpServletResponse response) throws Exception

AjaxTags란 Ajax 관련 오픈소스 사용을 위해 Model 객체의 데이터를 ’text/xml’ 형식으로 렌더링하는 View 클래스를 만들어 봤다.

package com.easycompany.view;
 
import java.io.PrintWriter;
import java.util.Map;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.web.servlet.view.AbstractView;
 
public class AjaxXmlView extends AbstractView {
 
	@Override
	protected void renderMergedOutputModel(Map model,
			HttpServletRequest request, HttpServletResponse response)
			throws Exception {
 
		response.setContentType("text/xml");
		response.setHeader("Cache-Control", "no-cache");
		response.setCharacterEncoding("UTF-8");
 
		PrintWriter writer = response.getWriter();
		writer.write((String) model.get("ajaxXml"));
		writer.close();
	}
}

Spring Tag Library

meassage tag(<spring:message>)

스프링은 메시지 리소스 파일로 부터 메시지를 가져와 간편하게 출력할수 있도록, <spring:message> 태그를 제공한다. JSP 페이지의 타이틀을 <spring:message>를 이용해서 출력하는 예제를 만들어 보자. 빈 정의 파일에 리소스 번들 관련된 설정이 되어 있어야 한다.

<!-- Message Source-->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"
	p:basename="messages"/>

먼저 메시지 관련 리소스 파일에 코드값을 설정해준다. PropertiedEditor 같은 유틸의 도움을 받으면 편리하게 한글 입력-편집이 가능하다.

/easycompany/webapp/WEB-INF/classes/messages_ko.properties

...
# -- spring:message --
easaycompany.loginform.title=로그인페이지
easaycompany.employeelist.title=사원 정보 리스트 페이지
easaycompany.updateemployee.title=사원 정보 수정 페이지
easaycompany.insertemployee.title=사원 정보 입력 페이지
easaycompany.departmentlist.title=부서 정보 리스트 페이지
easaycompany.updatedepartment.title=부서 정보 수정 페이지
easaycompany.insertdepartment.title=부서 정보 입력 페이지

JSP 페이지에 커스텀 태그를 사용하기 위해 라이브러리 선언을 해줘야 한다. 그리고 <spring:message> 태그의 code 값에는 메시지 키값을 주면 된다.

...
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title><spring:message code="easaycompany.departmentlist.title"/></title>
...

해당 화면의 타이틀이 “부서 정보 리스트 페이지”로 표기 될 것이다.

form tag(<form:form>,<form:input>,...)

폼 관련 어플리케이션을 개발할 때는 스프링이 제공하는 폼 태그와 같이 사용하면 편리하다. 스프링 폼 태그는 Model 데이터의 커맨드 객체(command object)나 참조 데이터(reference data)들을 화면상에서 쉽게 출력하도록 도와 준다. 일단, 스프링 폼 태그를 사용하려면 페이지에 커스텀 태그 라이브러리를 선언해야 한다.

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

스프링 폼 태그에는 아래와 같은 태그들이 있다.

form:form

<form:form>는 속성 commandName에 정의된 model attribute를 PageContext에 저장해서, <form:input>이나 <form:hidden>같은 tag들이 접근할 수 있도록 한다. 관련 속성은 아래와 같다.

  • commandName
    • 참조하려는 model attribute 이름
  • action
    • 폼 전송할 URL
  • method
    • 폼 전송시에 HTTP 메소드(GET, POST)
  • enctype
    • 폼 전송시에 데이터 인코딩 타입

<form:form> 태그를 사용하고 출력된 페이지에서 소스 보기로 HTML 코드를 열어보면 아래와 같이 HTML FORM 태그가 출력된걸 확인할 수 있을것이다.

<form:form commandName="department" action="http://myUrl..." method="post"> -> <form id="department" action="http://myUrl..." method="post">
 
<form:form commandName="department"> -> <form id="department" action="현재페이지 URL" method="post">

form:input

HTML text타입의 input 태그에 commandName에 지정된 객체 프로퍼티를 바인딩하기 위해 사용한다. path에 프로퍼티 이름을 적으면, text타입의 input 태그의 id, name 값이 프로퍼티 이름이 되고, value는 해당 프로퍼티의 값이 된다.

<form:form commandName="department">
	<tr>
		<th>부서이름</th>
		<td><form:input path="deptname" size="20"/></td>
	</tr>
</form:form>

아래와 같이 HTML로 출력된다.

<form id="department" action="/easycompany/updateDepartment.do?deptid=1100" method="post">
	<tr>
		<th>부서이름</th>
		<td><input id="deptname" name="deptname" type="text" value="회식메뉴혁신팀" size="20"/></td>
	</tr>
</form>

form:password

HTML password타입의 input 태그에 commandName에 지정된 객체 프로퍼티를 바인딩하기 위해 사용한다. 바인딩값을 표기하기 위해서는 showPassword 속성을 showPassword=“true”로 지정해 주어야 한다.

form:hidden

HTML hidden타입의 input 태그에 commandName에 지정된 객체 프로퍼티를 바인딩하기 위해 사용한다.

form:select, form:options, form:option

HTML select, option 태그에 commandName에 지정된 객체 프로퍼티를 바인딩하기 위해 사용한다. 아래와 같이 <form:select>의 path 속성에 commandName 객체의 프로퍼티를 지정하고, <form:options>의 items 속성에 List, Map등의 Collection 객체를 값으로 주면,

<form:form commandName="department">
	<tr>
		<th>상위부서</th>
		<td>
			<form:select path="superdeptid">
				<option value="">상위부서를 선택하세요.</option>
				<form:options items="${deptInfoOneDepthCategory}" />
			</form:select>
		</td>
	</tr>
</form:form>

아래와 같이 HTML로 출력된다. <form:select>의 path 속성값과 일치하는 option 값이 있으면 selected=“selected” 된다.

<form id="department" action="/easycompany/updateDepartment.do?deptid=1100" method="post">
	<tr>
		<th>상위부서</th>
		<td>
			<select id="superdeptid" name="superdeptid">
				<option value="">상위부서를 선택하세요.</option>
				<option value="5000">금융사업부</option>
                                <option value="3000">IT연구소</option>
                                <option value="4000">공공사업부</option>
                                <option value="1000" selected="selected">경영기획실</option>
                                <option value="2000">경영지원실</option>
			</select>
		</td>
	</tr>
</form>

form:checkboxes

form:checkbox

form:errors

전자정부프레임워크 Tag Library

<ui:pagination/>

페이징 처리의 편의를 위해 <ui:pagination/> 태그를 제공한다. <ui:pagination/>의 주요 속성은 아래와 같다.

이름설명필수여부
paginationInfo페이징리스트를 만들기 위해 필요한 데이터. 데이터 타입은 egovframework.rte.ptl.mvc.tags.ui.pagination.PaginationInfo이다.yes
type페이징리스트 렌더링을 담당할 클래스의 아이디. 이 아이디는 빈설정 파일에 선언된 프로퍼티 rendererType의 key값이다.yes
jsFunction페이지 번호에 걸리게 될 자바스크립트 함수 이름. 페이지 번호가 기본적인 argument로 전달된다.yes

ui 태그에 대한 라이브러리 선언을 해주고 페이징 리스트가 위치할 곳에 아래와 같이 사용하면 된다. paginationInfo 속성에는 Controller에서 Model 객체에 저장한 PaginationInfo의 attribute name을 적어 주면 되고, jsFunction 속성은 페이징 리스트의 각 페이지 번호에 걸릴 링크인 자바스크립트 함수명을 적어 주면 된다. type 속성은 빈 설정시에 rendererType 프로퍼티의 entry key값을 적어준다. 렌더링 타입을 태그에서 결정하는 것이다.

1. 관련 클래스 빈 설정한다.

<!-- For Pagination Tag -->	 
<bean id="imageRenderer" class="com.easycompany.tag.ImagePaginationRenderer"/>

<bean id="textRenderer" class="egovframework.rte.ptl.mvc.tags.ui.pagination.DefaultPaginationRenderer"/>

<bean id="paginationManager" class="egovframework.rte.ptl.mvc.tags.ui.pagination.DefaultPaginationManager">
    <property name="rendererType">
        <map>
            <entry key="image" value-ref="imageRenderer"/>
            <entry key="text" value-ref="textRenderer"/>
        </map>
    </property>
</bean>

2. JSP에서 라이브러리를 선언한 후 사용한다.

<%@ taglib prefix="ui" uri="http://egovframework.gov/ctl/ui"%>
...
<script type="text/javascript">
	function linkPage(pageNo){
		location.href = "/easycompany/employeeList.do?pageNo="+pageNo;
	}	
</script>
<body>
...
		<ui:pagination paginationInfo = "${paginationInfo}"
			type="image"
			jsFunction="linkPage"/>
...
</body>

<ui:pagination/>에 대한 좀더 상세한 설명과 사용법, 확장 방법등은 이곳을 참고

참고자료

  • The Spring Framework - Reference Documentation 2.5.6
  • Spring Framework API Documentation 2.5.6

3.11 - Map 객체를 통한 입력값 처리 방법

전자정부프레임워크 3.0 이전에는 CommandMapArgumentResolver를 통해 Map 객체를 사용했으나, 3.0부터는 @CommandMap과 AnnotationCommandMapArgumentResolver로 이를 처리한다. 이 클래스는 HTTP 요청의 파라미터 이름과 값을 Map에 담아 Controller에서 사용할 수 있도록 지원한다.

AnnotationCommandmapArgumentResolver

개요

Controller에서 화면(JSP) 입력값을 받기 위해서 일반적으로 Command(Form Class) 객체를 사용하지만, Map 객체를 사용하는걸 선호할 수 있다. 전자정부프레임워크 버전 3.0이전에서는 CommandMapArgumentResolver를 통해 Map객체를 사용할 수 있었다. 그러나 3.0부터는 @CommandMap과 AnnotationCommandmapArgumentResolver를 통해 Map객체를 사용할 수 있다. org.springframework.web.method.support.HandlerMethodArgumentResolver의 구현클래스인 AnnotationCommandMapArgumentResolver은 HTTP request 객체에 있는 파라미터이름과 값을 Map 객체에 담아 Controller에서 사용도록 제공한다.

설명

HandlerMethodArgumentResolver

Sping MVC의 @Controller의 메소드의 argument로 사용할 수 있는 유형(이에 관한 정보는 이곳을 참조하라.)은 기존의 계층형 Controller보다 다양해 졌지만, 필요에 따라 기본 유형외의 custom argument를 사용해야 할때가 있을 것이다. Sping MVC는 Controller의 argument 유형을 customizing 할 수 있는 HandlerMethodArgumentResolver라는 interface를 제공한다. 기존 Spring web 3.1이전 버전에서는 WebArgumentResolver를 구현하여 AnnotationMethodHandlerAdapter에 등록하여 ArgumentResolver를 적용하였으나, 3.1이후부터는 HandlerMethodArgumentResolver를 구현하여 RequestMappingHandlerAdapter에 등록하여 ArgumentResolver를 적용해야 한다.

아래와 같이 Controller의 메소드에서 MySpecialArg라는 custom argument를 argument로 사용하려 한다면,

@Controller
public class HelloController {
    public String hello(MySpecialArg mySpecialArg,...) {
       ...
       return "...";
    }
}

인터페이스 HandlerMethodArgumentResolver를 구현한 클래스를 만든다. 구현해야 할 메소드는 아래와 같다.

boolean supportsParameter(MethodParameter parameter);
 
Object resolveArgument(MethodParameter parameter,ModelAndViewContainer mavContainer, NativeWebRequest webRequest,WebDataBinderFactory binderFactory) throws Exception;
public class MySpecialArgumentResolver implements HandlerMethodArgumentResolver{
 
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		if(MySpecialArg.class.isAssignableFrom(parameter.getParameterType())) {
			return true;
		}
		else {
			return false;
		}
	}
 
	@Override
	public Object resolveArgument(MethodParameter parameter,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
			WebDataBinderFactory binderFactory) throws Exception {
		return new MySpecialArg();		
	}
}

만들어진 ArgumentResolver클래스는 RequestMappingHandlerAdapter의 customArgumentResolvers 프로퍼티에 등록하도록 한다. list유형 프로퍼티이므로 여러개의 ArgumentResolver클래스를 등록할 수 있다.

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    <property name="customArgumentResolvers">
        <list>
            <bean class="... MySpecialArgumentResolver" />
        </list>
    </property>
</bean>

AnnotationCommandMapArgumentResolver

HTTP request 객체에 있는 파라미터이름과 값을 특정 폼 빈에 담아서 사용하는 방식이 일반적이지만, Map 객체에 담아서 사용하는걸 선호하는 경우도 있다. Controller에서 Map객체를 쓰기 위해 전자정부 프레임워크 3.0부터는 @CommandMap과 AnnotationCommandMapArgumentResolver를 쓰도록 한다.

public String helloPost(@CommandMap Map commandMap, ModelMap model) {
...
}

Map commandMap에 파라미터의 이름과 값이 들어 있게 하려면 위에서 언급한 AnnotationCommandMapArgumentResolver를 이용해야 한다. HandlerMethodArgumentResolver의 구현 클래스인 AnnotationCommandMapArgumentResolver는 Controller 메소드의 argument중에 @CommandMap이 붙은 Map 객체가 있다면, HTTP request 객체에 있는 파라미터이름과 값을 Map객체에 담는다.

package egovframework.rte.ptl.mvc.bind;
...
public class AnnotationCommandMapArgumentResolver implements HandlerMethodArgumentResolver{
 
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		if(Map.class.isAssignableFrom(parameter.getParameterType()) 
				&& parameter.hasParameterAnnotation(CommandMap.class)) {
			return true;
		}
		else {
			return false;
		}
	}
 
	@Override
	public Object resolveArgument(MethodParameter parameter,
			ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
			WebDataBinderFactory binderFactory) throws Exception {
 
		Map<String, Object> commandMap = new HashMap<String, Object>();
		HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();			
		Enumeration<?> enumeration = request.getParameterNames();
 
		while (enumeration.hasMoreElements()) {
			String key = (String) enumeration.nextElement();
			String[] values = request.getParameterValues(key);
			if (values!=null) {
				commandMap.put(key, (values.length > 1) ? values:values[0] );
			}
		}
		return commandMap;
	}
}

AnnotationCommandMapArgumentResolver를 사용하려면 EgovRequestMappingHandlerAdapter에 등록해야 한다. 보통의 경우는 RequestMappingHandlerAdapter를 등록하여 사용하면 되지만, Controller에 Map객체를 쓰기 위해 AnnotationCommandMapArgumentResolver를 등록하려면 egov3.0부터 제공하는 EgovRequestMappingHandlerAdapter를 사용해야 한다. 만약 RequestMappingHAndlerAdapter를 이용하거나 <mvc:annotation-driven>을 사용할 경우에는 AnnotationCommandMapArgumentResolver를 사용할 수 없으므로 주의해야 한다.

<bean class="egovframework.rte.ptl.mvc.bind.annotation.EgovRequestMappingHandlerAdapter">
	<property name="customArgumentResolvers">
		<list>
			<bean class="egovframework.rte.ptl.mvc.bind.AnnotationCommandMapArgumentResolver" />
		</list>
	</property>
</bean>
 
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>

테스트를 위한 Form에 데이터를 아래와 같이 등록하고 폼을 제출했을때,

web-servlet-AnnotationCommandMapArgumentResolver

@CommandMap Map 객체의 데이터는 key, value 형태로 아래와 같이 들어 있다. 같은 파라미터 이름으로 여러값이 들어 있는 값은 배열로 들어 있다.

key:text1   value:{aaa,bbb}
key:text2   value:ccc
key:text3   value:ddd
key:cb   value:{on,on}
key:rb2   value:on
key:rb1   value:on

참고자료

3.12 - 스프링 WebFlux와 기존 스프링 웹 프레임워크 비교

스프링 WebFlux는 5.0 버전부터 추가된 리액티브 스택 웹 프레임워크로, 서블릿 API와는 달리 완전한 논블로킹 동작과 Reactive Streams back pressure를 지원한다. 기존의 스프링 웹 MVC와 함께 스프링 프레임워크에 포함되어 있으며, Netty, Undertow 등 다양한 서버에서 실행될 수 있다.

Web Reactive

개요

스프링 프레임워크, 스프링 웹 MVC를 포함한 기존 웹 프레임워크는 서블릿 API와 서블릿 컨테이너를 위해 개발되었다. 스프링 WebFlux는 5.0 버전부터 추가된 리액티브 스택 웹 프레임워크로서, 서블릿 API와 서블릿 컨테이너를 개발하기 위한 스프링 프레임워크이다. 스프링 웹 MVC를 포함한 기존의 웹 프레임워크와 달리 완전한 논블로킹으로 동작하며 Reactive Streams back pressure를 지원하고 Netty, Undertow, 서블릿 3.1+ 컨테이너 서버에서 실행된다. 웹 프레임워크 모두 스프링 프레임워크에 포함되어 있으며, 원하는 모듈을 선택하여 개발할 수 있다.

설명

Overview

스프링 WebFlux가 탄생한 이유 중 하나는 적은 쓰레드로 동시 처리를 제어하고 적은 하드웨어 리소스로 확장하기 위해 논블로킹 웹 스택이 필요했기 때문이다. 이전에도 서블릿 3.1은 논블로킹 I/O를 위한 API를 제공했지만 서블릿으로 논블로킹을 구현하려면 다른 동기 처리나(Filter, Servlet) 블로킹 방식(getParameter, getPart)을 쓰는 API를 사용하기 어려우므로 어떤 논블로킹과도 잘 동작하는 새로운 공통 API를 만들게 됐다. 또한 이미 비동기 논블로킹 환경에서 자리를 잡은 서버(e.g. Netty) 때문에라도 새 API가 필요했다.

또 다른 이유는 함수형 프로그래밍으로 자바 8에서 추가된 람다 표현식으로 자바에서도 함수형 API를 작성할 수 있게 되어 continuation-style API로 비동기 로직을 선언적으로 작성할 수 있다.

리액티브(Reactive) 정의

리액티브라는 용어는 변화에 반응하는 것을 중심에 두고 만든 프로그래밍 모델을 의미한다. 논블로킹은 작업을 기다리기보단 완료되거나 데이터를 사용할 수 있게 되면 반응하므로 논블로킹도 리액티브이다. 스프링은 리액티브와 관련한 중요한 메커니즘이 하나 더 있는데, 논블로킹 back pressure이다. 동기식 명령형 코드에서 블로킹 호출은 호출자가 대기하도록 하는 자연스러운 형태의 back pressure 역할을 한다. 논블로킹 코드에서는 프로듀서 속도가 컨슈머 속도롤 압도하지 않도록 이벤트 속도를 제어하는 것이 중요하다.

리액티브 스트림은 비동기 구성 요소 간의 상호 작용을 정의하는 간단한 사양(Java 9에서도 채택됨)으로, back pressure가 있는 비동기 구성 요소 간의 상호 작용을 정의한다. 예를 들어 데이터 저장소(Publisher 역할)가 데이터를 생성하면 HTTP 서버(Subscriber 역할)가 이 데이터로 요청을 처리할 수 있다. 리액티브 스트림의 주요 목적은 Subscriber가 Publisher의 데이터 생성 속도를 제어할 수 있도록 하는 것이다.

자주 묻는 질문 : Publisher 속도를 늦출 수 없으면 어떻게 할까?
리액티브 스트림의 목적은 메커니즘과 경계를 확립하는 것이다. Publisher가 속도를 늦출 수 없다면 데이터를 버퍼에 담을지(Buffer), 데이터를 삭제할지(Drop), 실패(Fail)로 처리할지를 결정해야 한다.

리액티브 API

리액티브 스트림은 상호 운용성을 위해 중요한 역할을 한다. 하지만 이건 라이브러리와 인프라 구조에 사용되는 컴포넌트에는 유용하지만 애플리케이션 API에서 다루기엔 너무 저수준이다. 애플리케이션은 컬렉션 뿐만 아니라 비동기 로직을 구성하기 위해 더 높은 수준의 더 풍부하고 기능적인 API가 필요하다. 이는 Java 8 Stream API와 유사하며 이것이 바로 리액티브 라이브러리가 하는 역할이다.

Reactor는 스프링 WebFlux가를 위해 선택된 리액티브 라이브러리이다. 이 라이브러리는 0…1(Mono) 및 0…N(Flux)의 데이터 시퀀스에서 작업할 수 있는 Mono 및 Flux API 유형을 제공하며, ReactiveX의 연산자 어휘와 일치하는 풍부한 연산자 집합을 통해 작동한다. Reactor는 리액티브 스트림 라이브러리이므로 모든 연산자는 논블로킹 back pressure를 지원한다. Reactor는 서버 측 Java에 중점을 두고 스프링과 긴밀히 협력하여 개발되었다.

WebFlux는 핵심 종속성으로 Reactor를 필요로 하지만, 리액티브 스트림를 통해 다른 리액티브 라이브러리와 상호 운용할 수 있다. 일반적으로 WebFlux API는 일반 Publisher를 입력으로 받아 내부적으로 Reactor 유형에 맞게 조정하고 이를 사용한 후 Flux 또는 Mono를 출력으로 반환한다. 따라서 모든 Publisher를 입력으로 전달할 수 있고 출력에 연산을 적용할 수 있지만 다른 리액티브 라이브러리와 함께 사용하려면 출력을 조정해야 한다. 가능한 경우(예: 주석이 달린 컨트롤러) WebFlux는 RxJava나 다른 리액티브 라이브러리에 맞게 바꿔준다. 자세한 내용은 리액티브 라이브러리를 참조하라.

Programming Models

스프링 웹 모듈에 있는 WebFlux는 여러 서버를 지원하기 위한 HTTP 추상화와 리액티브 스트림 어댑터, 코덱, Servlet APT에 상응하는 핵심 웹 핸들러 API 등 스프링 WebFlux의 기반이 되는 리액트브 기반이 포함되어 있다. 이러한 기분 위에서 스프링 WebFlux는 두 가지 프로그래밍 모델 중 하나를 선택할 수 있다.

  • Annotated Controllers : 스프링 MVC와 일치하며 스프링 웹 모듈의 동일한 주석을 기반으로 한다. 스프링 MVC와 WebFlux 컨트롤러는 모두 리액티브(Reactor 및 RxJava) 반환 유형을 지원하므로 구분하기가 쉽지 않다. 한 가지 주목할 만한 차이점은 WebFlux가 리액티브 @RequestBody 인수도 지원한다는 것이다.
  • Functional Endpoints : 경량화된 람다 기반 함수형 프로그래밍 모델로 요청을 라우팅해주는 작은 라이브러리나 유틸리티 모음이라고 생각하면 된다. Annotated Controllsers와 다른 점은 애플리케이션이 처음부터 끝까지 요청 처리를 담당하고 어노테이션을 통해 의도를 선언한 후 다시 호출된다는 점이다.

Applicability

스프링 MVC와 WebFlux 중 어떤 것을 적용할 것이냐는 이분법적 사고는 좋지 않다. 사실, 두 가지 모두 함께 작동하여 사용 가능한 옵션의 범위를 확장한다. 이 둘은 서로의 연속성과 일관성을 위해 설계되었으며, 나란히 사용할 수 있고, 각 측면의 피드백은 양쪽 모두에게 도움이 된다. 다음 다이어그램은 이 두 가지의 관계, 공통점, 그리고 각각 고유하게 지원하는 기능을 보여준다.

다음과 같은 구체적인 사항을 고려하는 것이 좋다.

image

  • 정상적으로 작동하는 스프링 MVC 애플리케이션이 있다면 변경할 필요가 없다. 명령형 프로그래밍은 코드를 작성하고, 이해하고, 디버깅하는 가장 쉬운 방법이다. 역사적으로 대부분의 라이브러리가 차단되어 있기 때문에 라이브러리 선택의 폭이 넓습니다.
  • 이미 논블로킹 웹 스택을 찾고 있다면, 스프링 WebFlux는 이 분야의 다른 제품과 동일한 실행 모델 이점을 제공하며 서버(Netty, Tomcat, Jetty, Undertow 및 Servlet 3.1+ 컨테이너), 프로그래밍 모델(Annotated Controllers and Functional Endpoints), 반응형 라이브러리(Reactor, RxJava 또는 기타) 선택도 제공한다.
  • Java 8 람다 또는 Kotlin과 함께 사용하기 위한 가볍고 기능적인 웹 프레임워크에 관심이 있다면 스프링 WebFlux 함수형 웹 엔드포인트를 사용할 수 있다. 로직을 투명하게 제어할 수 있기 때문에 덜 복잡한 요구 사항을 가진 소규모 애플리케이션이나 마이크로서비스에도 좋은 선택이 될 수 있다.
  • 마이크로서비스 아키텍처에서는 스프링 MVC 또는 스프링 WebFlux 컨트롤러를 사용하거나 스프링 WebFlux Functional Endpoints를 사용하는 애플리케이션을 혼합하여 사용할 수 있다. 두 프레임워크 모두에서 동일한 어노테이션 기반 프로그래밍 모델을 지원하므로 지식을 재사용하는 동시에 작업에 적합한 도구를 선택하기 쉽다.
  • 애플리케이션을 평가하는 간단한 방법은 애플리케이션의 종속성을 확인하는 것이다. 블로킹 지속성 API(JPA, JDBC) 또는 네트워킹 API를 사용해야 하는 경우, 적어도 일반적인 아키텍처에서는 스프링 MVC가 가장 좋다. 별도의 스레드에서 블로킹 호출을 수행하는 것은 기술적으로 Reactor와 RxJava 모두에서 가능하지만 논블로킹 웹 스택을 활용하기 어렵다.
  • 원격 서비스에 대한 호출이 있는 스프링 MVC 애플리케이션이 있는 경우 리액티브 WebClient를 사용하면 스프링 MVC 컨트롤러 메서드에서 직접 리액티브 유형(Reactor, RxJava 또는 기타)을 반환할 수 있다. 호출 당 지연 시간이 길거나 호출 간의 상호 의존성이 클수록 이 점은 더욱 극적으로 나타난다. 스프링 MVC 컨트롤러는 다른 리액티브 컴포넌트도 호출할 수 있다.
  • 팀 규모가 크다면 논블로킹, 함수형, 선언적 프로그래밍은 러닝커브가 높다는 점도 고려해야 한다. 전면적인 전환없이 시작할 수 있는 실용적인 방법은 리액티브 웹클라이언트를 사용하는 것이다. 작은 것부터 시작해서 변화가 있는지 확인하면 전환이 불필요할 경우도 많을 것이다. 어떤 이점을 찾아야 할지 잘 모르겠다면 논블로킹 I/O의 작동 방식(예: 단일 스레드 Node.js의 동시 처리)과 그 효과에 대해 알아보는 것부터 시작해라.

Servers

스프링 WebFlux는 Tomcat, Jetty, Servlet 3.1+ 컨테이너 뿐만 아니라 Netty 및 Undertow도 지원된다. 모든 서버는 낮은 수준의 공통 API에 맞춰 조정되므로 서버 전반에서 상위 수준의 프로그래밍 모델을 지원할 수 있다.

스프링 WebFlux에는 서버 기동이나 중단을 위한 내장 기능은 없다. 하지만 스프링 설정과 WebFlux를 조립해 구성한 코드로 쉽게 애플리케이션을 실행할 수 있다.

스프링 Boot에는 이러한 단계를 자동화하는 WebFlux 스타터가 있다. 기본적으로 스타터는 Netty를 사용하지만, Maven 또는 Gradle 종속성을 변경하여 Tomcat, Jetty 또는 Undertow로 쉽게 전환할 수 있다. 스프링 Boot가 Netty를 디폴트로 사용하는 이유는 보통 비동기 논블로킹에 많이 사용되기도 하고 클라이언트와 서버가 리소스를 공유할 수 있어서다.

Tomcat과 Jetty는 스프링 MVC와 WebFlux에 모두 사용할 수 있다. 하지만 동작방식은 다르다는 점에 주의해야 한다. 스프링 MVC는 Servlet 차단 I/O에 의존하며, 애플리케이션이 필요한 경우 Servlet API를 직접 사용할 수 있다. 스프링 WebFlux는 Servlet 3.1 논블로킹 I/O롤 동작하며 서블릿 API는 저수준 어댑터로 사용하기 때문에 노출되어 있지 않다.

스프링 WebFlux에서 Undertow를 사용할 때는 서블릿 API가 아닌 Undertow API를 사용한다.

Performance

성능에는 많은 특징과 의미가 있다. 리액티브 및 논블로킹은 일반적으로 애플리케이션을 더 빠르게 실행하지 않는다. 일부 경우(예: 웹클라이언트를 사용하여 원격 호출을 병렬로 실행하는 경우)에는 그럴 수 있다. 전반적으로 논블로킹 방식으로 작업을 수행하려면 더 많은 작업이 필요하며 필요한 처리 시간이 약간 늘어날 수 있다.

리액티브 및 논블로킹 방식의 주요 이점은 적은 수의 고정된 스레드와 적은 메모리로 확장할 수 있다는 점이다. 따라서 애플리케이션이 보다 예측 가능한 방식으로 확장되므로 부하가 걸렸을 때 복원력이 향상된다. 그러나 이러한 이점을 누리려면 느리고 예측할 수 없는 네트워크 I/O가 혼합된 경우 등 지연 시간이 어느 정도 발생해야 한다. 바로 이 지점에서 리액티브 스택의 강점이 드러나기 시작하며, 그 차이는 극적일 수 있다.

Concurrency Model

스프링 MVC와 스프링 WebFlux는 모두 Annotated Controller를 사용할 수 있다는 점은 동일해도 동시성 모델과 블로킹/스레드 기본 전략은 다르다. 스프링 MVC(그리고 일반적인 서블릿 애플리케이션)는 애플리케이션이 처리중인 스레드가 잠시 중단될 수 있다(예를 들어 원격 호출의 경우). 이러한 이유로 서블릿 컨테이너는 요청 처리 중에 잠재적인 차단을 흡수하기 위해 대규모 스레드 풀을 사용한다. 스프링 WebFlux(및 일반적으로 논블로킹 서버)에서는 실행되는 스레드가 중단되지 않는다는 전제가 있다. 따라서 논블로킹 서버는 작은 고정 크기의 스레드 풀(이벤트 루프 워커)을 사용하여 요청을 처리한다.

"확장"과 "적은 수의 스레드"는 모순적으로 들릴 수 있지만 현재 스레드를 차단하지 않고 대신 콜백에 의존한다는 것은 흡수할 차단 호출이 없기 때문에 추가 스레드가 필요하지 않다는 것을 의미한다.

Invoking a Blocking API

블로킹 라이브러리를 사용해야 한다면 어떻게 해야 할까? Reactor와 RxJava는 모두 다른 스레드에서 처리를 계속할 수 있는 PublishOn 연산자를 제공한다. 즉, 쉽게 빠져나갈 수 있는 탈출구가 있다는 뜻이다. 그러나 블로킹 API는 이 동시성 모델에 적합하지 않다는 점에 유의해야 한다.

Mutable State

Reactor와 RxJava에서는 연산자를 통해 로직을 선언한다. 런타임에 데이터가 별개의 단계에서 순차적으로 처리되는 리액티브 파이프라인이 형성된다. 이 파이프라인의 주요 이점은 해당 파이프라인 내의 애플리케이션 코드가 동시에 호출되지 않기 때문에 애플리케이션이 변경 가능한 상태를 보호할 필요가 없다는 것이다.

Threading Model

스프링 WebFlux를 사용하는 애플리케이션은 어떤 스레드를 얼마나 실행할까?

  • 최소한의 설정으로 스프링 WebFlux 서버를 띄우면(예를 들어 데이터 접근이나 다른 의존성이 없는) 서버는 한 개의 스레드를 운영하고 소량의 스레드로 요청을 처리할 수 있다(보통은 CPU 코더 수만큼). 하지만 서블릿 컨테이너는 서블릿 블로킹 I/O와 서블릿 3.1 논블로킹 I/O를 모두 지원하기 때문에 더 많은 스레드(예들 들어 Tomcat의 경우 10개)를 실행할 것이다.
  • 리액티브 WebClient는 이벤트 루프를 사용한다. 따라서 적은 스레드를 고정해 두고 쓴다(예를 들어 Reactor Nettry 커넥터를 쓴다면 reactor-http-nio-로 시작하는 스레드를 확인할 수 있다.). 단 클라이언트와 서버에서 모두 Reactor Netty를 사용하면 디폴트로 이벤트 루프 리소스를 공유한다.
  • Reactor와 RXJava는 스케줄러 라는 추상화된 스레드 풀 전략을 제공한다. publishOn 연산자가 나머지 연산을 다른 스레드 풀로 전환할 때도 이 스케줄러를 사용한다. 스케줄러는 이름을 보면 동시 처리 전략을 알 수 있다. 예를 들어 제한된 스레드로 CPU 연산이 많은 처리를 할 때는 parallel, 여러 스레드로 I/O가 많은 처리를 할 때는 elastic이다. 이런 스레드를 본다면 코드 어딘가에서 그 이름에 해당하는 스레드 풀 스케줄러 전략을 사용하고 있다는 뜻이다.
  • 데이터에 접근하는 라이브러리나 다른 외부 의존성에서 스레드를 따로 실행하는 경우도 있다.

Configuring

스프링 프레임워크에서 서버를 직접 실행하거나 중단할 수 없다. 서버의 스레딩 모델을 구성하려면 서버별 구성 API를 사용하거나 스프링 Boot를 사용하는 경우 각 서버에 대한 스프링 Boot 구성 옵션을 확인해야 한다. WebClient를 직접 구성할 수도 있다. 다른 라이브러리의 경우 해당 라이브러리 설명서를 참고하라.

참고 자료

3.13 - Reactive Core

스프링-웹 모듈은 서버 요청 처리를 위한 HttpHandler와 WebHandler API를 제공하며, 클라이언트 사이드에서는 논블로킹 I/O와 리액티브 스트림 기반의 WebClient가 지원된다. 또한, 서버와 클라이언트 모두 HTTP 요청 및 응답 콘텐츠의 직렬화와 역직렬화를 위한 코덱 기능을 포함하고 있다.

Reactive Core

설명

스프링-웹 모듈에는 반응형 웹 애플리케이션을 위한 다음과 같은 기본 지원이 포함되어 있다.

  • 서버 요청 처리에는 두 가지 수준을 지원한다.
    • HttpHandler: 논블로킹 I/O 및 리액티브 스트림 Back Pressure을 사용하는 HTTP 요청을 처리하며, Reactor Netty, Undertow, Tomcat, Jetty 및 모든 Servlet 3.1+ 컨테이너용 어댑터와 함께 사용한다.
    • WebHandler API: 요청 처리를 위한 약간 더 높은 수준의 범용 웹 API로, 주석이 달린 컨트롤러 및 기능적 엔드포인트와 같은 구체적인 프로그래밍 모델로 작성되어 있다.
  • 클라이언트 사이드에서는 논블로킹 I/O 및 리액티브 스트림 Back Pressure으로 HTTP 요청을 수행하는 기본 ClientHttpConnector 계약과 함께 Reactor Netty, 리액티브 Jetty HttpClient 및 Apache HttpComponents용 어댑터가 있다. 애플리케이션에 사용되는 상위 수준의 WebClient는 이를 기반으로 구축된다.
  • 클라이언트와 서버 모두 코덱으로 HTTP 요청 및 응답 콘텐츠의 직렬화 및 역직렬화를 한다.

HttpHandler

HttpHandler는 요청과 응답을 처리하는 메소드를 하나만 가지고 있다. 의도적으로 최소한의 기능을 제공하며, 주된 목적은 다양한 HTTP 서버 API에 대한 최소한의 추상화이다. 지원하는 서버의 API는 아래 표와 같다.

서버 이름사용하는 Servlet API리액티브 스트림 지원
NettyNetty APIReactor Netty
UndertowUndertow API APIspring-web: Undertow to 리액티브 스트림 브릿지
Tomcat서블릿 3.1 논블로킹 I/O; ByteBuffers로 byte[]를 읽고 쓰는 Tomcat APIspring-web: 서블릿 3.1 논블로킹 I/O to 리액티브 스트림 브릿지
Jetty서블릿 3.1 논블로킹 I/O; ByteBuffers로 byte[]를 읽고 쓰는 Jetty APIspring-web: 서블릿 3.1 논블로킹 I/O to 리액티브 스트림 브릿지
서블릿 3.1 컨테이너서블릿 3.1 논블로킹 I/Ospring-web: 서블릿 3.1 논블로킹 I/O to 리액티브 스트림 브릿지

서버 Dependency는 아래 표와 같다(지원 버전 참고)

서버 이름Group IDArtifact Name
Reactor Nettyio.projectreactor.nettyreactor-netty
Undertowio.undertowundertow-core
Tomcatorg.apache.tomcat.embedtomcat-embed-core
Jettyorg.eclipse.jettyjetty-server, jetty-servlet

다음은 각 서버의 API 어댑터를 활용하는 HttpHandler이다.

리액터 Netty

HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bind().block();

Undertow

HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();

Tomcat

HttpHandler handler = ...
Servlet servlet = new TomcatHttpHandlerAdapter(handler);

Tomcat server = new Tomcat();
File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = server.addContext("", base.getAbsolutePath());
Tomcat.addServlet(rootContext, "main", servlet);
rootContext.addServletMappingDecoded("/", "main");
server.setHost(host);
server.setPort(port);
server.start();

Jetty

HttpHandler handler = ...
Servlet servlet = new JettyHttpHandlerAdapter(handler);

Server server = new Server();
ServletContextHandler contextHandler = new ServletContextHandler(server, "");
contextHandler.addServlet(new ServletHolder(servlet), "/");
contextHandler.start();

ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);
server.start();

서블릿 3.1+ 컨테이너

서블릿 3.1+ 컨테이너에 WAR를 배포하려면 WAR에 AbstractReactiveWebInitializer를 확장하여 추가하면 된다. 이 클래스는 HttpHandler, ServletHttpHandlerAdapter를 감싸고 있으며, 이 핸들러를 서블릿으로 등록한다.

WebHandler API

org.springframework.web.server 패키지를 보면 HttpHandler가 WebHandler와 여러 WebExceptionHandler, WebFilter로 체인을 형성해 요청을 처리하는 범용 웹 API를 제공한다. WebHttpHandlerBuilder에 컴포넌트를 등록하거나 스프링 ApplicationContext 위치만 알려주면 자동으로 컴포넌트 체인에 추가한다. HttpHandler는 서로 다른 HTTP 서버를 쓰기 위한 추상화가 전부인 반면, WebHandler API는 아래와 같이 웹 애플리케이션에서 흔히 쓰는 광범위한 기능을 제공한다.

  • User session과 Session attributes
  • Request attributes.
  • Locale, Principal 리졸브
  • form 데이터 파싱, 캐싱 조회
  • multipart 데이터 추상화
  • 기타 등등

Form Data

ServerWebExchange는 form 데이터에 접근할 수 있는 메소드를 제공한다.

Mono<MultiValueMap<String, String>> getFormData();

DefaultServerWebExchange는 설정에 있는 HttpMessageReader를 사용해 form 데이터를 MultiValueMap으로 파싱한다. 디폴트로 사용하는 리더는 ServerCodecConfigurer 빈에 있는 FormHttpMessageReader이다.

Multipart Data

ServerWebExchange는 multipart 데이터에 접근할 수 있는 메소드를 제공한다.

Mono<MultiValueMap<String, Part>> getMultipartData();

DefaultServerWebExchange는 설정에 있는 HttpMessageReader<MultiValueMap<String, Part»를 사용해 multipart/form-data 컨텐츠를 MultiValueMap으로 파싱한다. 현재로써는 Synchronoss NIO Multipart가 유일하게 지원하는 서브파티 라이브러리이며, 논블로킹으로 multipart 요청을 파싱하는 유일한 라이브러리이다. ServerCodecConfigurer 빈으로 활성화할 수 있다. 스트리밍 방식으로 multipart 데이터를 파싱하려면 HttpMessageReader가 리턴하는 Flux를 사용한다. 예를 들어 컨트롤러에서 @RequestPart를 선언하면 Map처럼 이름으로 각 파트에 접근하겠다는 뜻이므로 multipart 데이터를 한번에 파싱해야 한다. 반대로 Flux 타입에 @RequestPart를 사용허면 컨텐츠를 디코딩할 때 MultiValueMap에 수집하지 않는다.

Forwarded Headers

Load Balancers와 같이 프톡시를 경유한 요청은 호스트, 포트, URL 스킴이 변경될 수 있어서 클라이언트 입장에서는 원래의 URL 정보를 알아내기 어렵다. RFC 7239 정의에 따르면 Forwarded HTTP 헤더는 프록시가 원래 요청에 대한 정보를 추가하는 헤더이다. X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Proto, X-Forwarded-Ssl, and X-Forwarded-Prefix 같은 비표준 헤더도 존재한다. ForwardedHeaderTransformer는 forwarded 헤더를 보고 요청의 호스트, 포트, 스킴을 바꿔준 다음, 헤더를 제거하는 컴포넌트이다. ForwardedHeaderTransformer라는 이름으로 빈을 정의하면 자동으로 체인에 추가된다. forwarded 헤더는 보안에 신경써야 할 요소가 있는데 프록시가 헤더를 추가한건지, 클라이언트가 악의적으로 추가한 것인지 애플리케이션에서는 알 수 없기 때문이다. 따라서 외부에서 들어오는 신뢰할 수 없는 프록시 요청을 제거할 때 ForwardedHeaderTransformer를 removeOnly=true로 설정하여 헤더 정보를 사용하지 않고 제거할 수 있다.

5.1 버전부터 ForwardedHeaderFilter는 제거대상(deprecated)이 되어 ForwardedHeaderTransformer를 대신한다. 따라서 exchange(http 요청/응답과 세션 정보 등의 컨테이너)를 만들기 전에 forwarded 헤더를 처리할 수 있다. 필터를 사용하더라도 이 필터는 전체 필터 리스트에서 제외되며 그 대신 ForwardedHeaderTransformer를 사용한다.

Filters

WebHandler API에서는 WebFilter를 사용하면 다른 필터 체인과 WebHandler 전후에 요청을 가로채서 원하는 로직을 넣을 수 있다. WebFilter를 등록하려면 스프링 빈으로 만들어 빈 위에 @Order를 선언아거나 Ordered를 구현해 순서를 정해도 되고, WebFlux Config를 사용해도 된다.

CORS

CORS는 컨트롤러에 어노테이션을 선언하는 것으로 잘 동작한다. Spring Security와 사용하면 CorsFilter를 사용해서 Spring Security 필터 체인보다 먼저 처리하도록 해야 한다.

Exceptions

WebHandler API에서는 WebFilter 체인과 WebHandler에서 발생한 예외를 WebExceptionHandler로 처리한다. WebExceptionHandler를 등록하려면 스프링 빈으로 빈 위에 @Order를 선언하거나 Ordered를 구현해 순서를 정해도 되고, WebFlux Config를 사용해도 된다. 아래 표는 바로 사용할 수 있는 WebExceptionHandler 구현체이다.

Exception HandlerDescription
ResponseStatusExceptionHandlerHttp status code를 지정할 수 있는 ResponseStatusException을 처리한다.
WebFluxResponseStatusExceptionHandlerResponseStatusExceptionHandler를 확장하는 것으로 다른 exception 타입도 @ResponseStatus를 선언해서 HTTP status code를 정할 수 있다.
이 핸들러는 WebFlux Config 안에 선언되어 있다.

참고자료

3.14 - 스프링 WebFlux의 DispatcherHandler와 요청 처리 흐름

스프링 WebFlux는 프론트 컨트롤러 패턴을 사용하며, DispatcherHandler가 중앙 WebHandler로서 요청을 다른 컴포넌트에 위임한다. DispatcherHandler는 스프링 설정에 따라 다양한 워크플로우를 지원하고, ApplicationContextAware 인터페이스를 구현해 실행 중인 컨텍스트에 접근할 수 있다.

DispatcherHandler

설명

스프링 WebFlux도 스프링 MVC와 유사한 프론트 컨트롤러 패턴을 사용한다. 중앙 WebHandler가 받은 요청을 다른 컴포넌트에 위임하는데 DispatcherHandler가 바로 이 중앙 WebHandler다. 이 모델 덕분에 다양한 워크플로우를 지원할 수 있다. DispatcherHandler는 스프링 설정에 따라 그에 맞는 컴포넌트로 위임한다. DispatcherHandler도 스프링 빈이며 ApplicationContextAware 인터페이스를 구현했기 때문에 실행중인 컨텍스트에 접근할 수 있다. DispatcherHandler 빈을 WebHandler 이름으로 정의하면 WebHttpHandlerBuilder가 감지하고 WebHandler API에서 설명했던 체인에 추가한다.

WebFlux 애플리케이션에서 사용하는 일반적인 스프링 설정은 다음과 같다.

  • webHandler 이름의 DispatcherHandler 빈
  • WebFilter, WebExceptionHandler 빈
  • 그 외 DispatcherHandler가 사용하는 빈
  • 기타 등등

WebExceptionHandler가 체인을 만들 때 아래와 같이 사용한다.

ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();

Special Bean Types

DispatcherHandler는 요청을 처리하고 그에 맞는 응답을 만들 때 사용하는 특별한 빈이 있다. 특별한 빈이란 WebFlux 프레임워크가 동작하는데 필요하며 스프링이 관리하는 Object 인스턴스를 말한다. 이 빈들은 기본적으로 내장돼 있지만 프로퍼티를 수정해서 확장하거나 커스텀 빈으로 대체할 수도 있다.

DispatcherHandler는 다음과 같은 빈을 감지한다. 저수준에서 동작하는 다른 빈도 자동으로 추가될 수 있다는 점에 주의하라

Bean typeExplanation
HandlerMapping요청을 핸들러에 매핑한다. 매핑 기준은 HandlerMapping 구현체마다 다른다(어노테이션을 선언한 컨트롤러, URL 패턴 매칭 등)
주로 쓰는 구현체는 @RequestMapping을 선언한 메소드를 찾는 RequestMappingHandlerMapping, 함수형 엔드포인트를 라이팅하는 RouterFunctionMapping, URL 패스 패턴으로 WebHandler를 찾는 SimpleUrlHandlerMapping 등이 있다.
HandlerAdapter핸들러가 실제로 호출되는 방식에 관계없이 요청에 매핑된 핸들러를 호출할 수 있도록 DispatcherHandler를 지원한다.
예를 들어 어노테이션이 있는 컨트롤러를 호출하려면 어노테이션을 해결해야 한다.
HandlerAdapter의 주요 목적은 이러한 세부 사항으로부터 DispatcherHandler를 보호하는 것이다.
HandlerResultHandler핸들러 호출의 결과를 처리하고 응답을 종료한다. Result Handling을 참고하라.

WebFlux Config

애플리케이션은 요청을 처리하는 데 필요한 인프라 빈(웹 핸들러 API 및 DispatcherHandler)을 선언할 수 있다. 그러나 대부분의 경우 WebFlux Config로 시작하는게 좋다. 이 구성은 필요한 빈을 선언하고 이를 사용자 정의할 수 있는 상위 수준의 구성 콜백 API를 제공한다.

스프링 부트를 사용해도 이 WebFlux Config로 초기화하며 부트가 제공하는 옵션으로 좀 더 편리하게 설정을 관리할 수 있다.

Processing

DispatcherHandler는 다음과 같이 요청을 처리한다.

  • HandlerMapping에서 매칭되는 핸들러를 찾는다. 첫번째로 매칭된 핸들러를 사용한다.
  • 핸들러를 찾으면 적당한 HandlerAdapter를 사용해 핸들러를 실행하고 HandlerResult를 돌려 받는다.
  • HandlerResult를 적절한 HandlerResultHandler로 넘겨 바로 응답을 만들거나 뷰를 랜더링하고 처리를 완료한다.

Result Handling

핸들러 호출의 반환 값은 HandlerAdapter를 통해 몇 가지 추가 컨텍스트와 함께 HandlerResult로 래핑되어 지원을 요청하는 첫 번째 HandlerResultHandler로 전달된다. 아래 표는 사용 가능한 HandlerResultHandler 구현을 보여 주며, 모두 WebFlux 구성에 선언되어 있다.:

Result Handler TypeReturn ValuesDefault Order
ResponseEntityResultHandlerResponseEntity, 보통은 @Controller에서 사용0
ServerResponseResultHandlerServerResponse, 보통은 Functional Endpoints에서 사용0
ResponseBodyResultHandler@ResponseBody, @RestController에서 리턴한 값을 처리100
ViewResolutionResultHandlerCharSequence, View, Model, Map, Rendering이나 다른 Object를 model attribute로 처리Integer.MAX_VALUE

Exceptions

HandlerAdapter가 리턴한 HandlerResult는 핸들러마다 다른 에러 처리 함수에 넘겨진다. 이 함수는 아래와 같을 때 호출된다.

  • 핸들러 실행에 실패한 경우(예를 들어 @Controller)
  • HandlerResultHandler가 핸들러가 리턴한 값을 처리하는데 실패한 경우

핸들러가 리턴한 리액티브 타입이 데이터를 produce 하기 전에 에러를 알아차릴 수 있으면 이 함수로 응답을 변경할 수 있다.(예를 들어 status로) 이로 인해 @Controller 클래스의 특정 메소드에 @ExceptionHandler를 선언할 수 있다. 스프링 MVC에선 HandlerExceptionResolver가 이 역할을 담당한다. 여기서 중요한 것은 MVC가 아니지만 WebFlux 핸들러를 선택하기 전 발생한 Exception은 @ControllerAdvice로 처리할 수 없다.

View Resolution

뷰 해상도를 사용하면 특정 뷰 기술에 종속되지 않고 HTML 템플릿과 모델을 사용하여 브라우저에 렌더링할 수 있다. WebFlux에서 뷰 해상도는 ViewResolver 인스턴스를 사용하여 String을 View 인스턴스에 매핑하는 전용 HandlerResultHandler를 통해 지원된다. 그런 다음 뷰를 사용하여 응답을 렌더링한다.

Handling

ViewResolutionResultHandler로 전달된 HandlerResult에는 핸들러의 반환 값과 요청 처리 중에 추가된 속성이 포함된 모델이 포함된다. 반환 값은 다음 중 하나로 처리된다.

  • 문자열, 문자 시퀀스: 구성된 뷰리졸버 구현 목록을 통해 뷰로 확인할 논리적 뷰 이름이다.
  • void: 요청 패스에 맞는 디폴트 뷰 이름에서 앞뒤 슬래쉬를 제거하고 뷰로 리졸브한다. 뷰 이름이 제공되지 않거나(예를 들어 model attribute를 리턴한 경우) 비동기 값일 때도(예를 들어 Mono가 비어 있을 때) 동일하게 처리한다.
  • 렌더링: 뷰 해상도 시나리오를 위한 API입니다. 코드 완성 기능이 있는 IDE에서 옵션을 확인해라.
  • Model, Map: 요청에 대해 모델에 추가할 model attributes
  • 기타: 다른 모든 반환 값(BeanUtils#isSimpleProperty가 true를 리턴하는 값은 제외)은 모델에 추가할 model attribute로 간주된다. @ModelAttribute 어노테이션이 없으면 conventions와 클래스명으로 속성 이름을 결정한다.

모델에는 비동기 리액티브 타입도 있을 수 있다(예를 들어 리액터나 RxJava가 리턴한 값). 이런 model attribute는 AbstractView가 랜더링하기 전에 실제값으로 바꿔준다. single-value 리액티브 타입은 비어있지 않다면 값 하나로 리졸브되고 multi-value 리액티브 타입(에를 들어 Flux)은 List로 수집된다.

뷰 리졸브는 스프링 설정에 ViewResolutionResultHandler만 추가하면된다. WebFlux Config는 뷰 리졸브를 위한 설정 API를 제공한다.

Redirecting

리액티브는 뷰 이름에 redirect: 프리픽스를 붙이기만 하면 된다. UrlBasedViewResolver(히위 클래스도 포함)가 이를 리다이렉트 요청으로 판단한다. 프리픽스를 제외한 나머지 뷰 이름은 리다이렉트 URL로 사용한다. 동작 자체는 컨트롤러가 RedirectView나 Rendering.redirectTo(“abc”).build()를 리턴했을 때와 동일하지만, 이 방법은 컨트롤러가 직접 뷰 이름을 보고 처리한다. redirect:/some/resource 같은 값은 현재 애플리케이션에서 이동할 페이지를 찾고 redirect:https://example.com/arbitrary/path 같이 사용하면 해당 URL로 리다이렉트한다.

Content Negotiation

content negotiation은 ViewResolutionResultHandler가 담당한다. 요청 미디어 타입과 View가 지원하는 미디어 타입을 비교해서 첫번째로 찾은 View를 사용한다. 스프링 WebFlux는 HttpMessageWriter로 Json, XML 같은 미디어 타입을 만드는 HttpMessageWriterView를 지원한다. 보통은 WebFlux 설정을 통해 HttpMessageWriterView를 디폴트 뷰로 사용한다. 디폴트 뷰는 요청 미디어 타입과 일치하기만 하면 항상 사용되는 뷰다.

참고자료

3.15 - 스프링 WebFlux에서 어노테이션 기반 컨트롤러 활용 방법

스프링 WebFlux는 @Controller, @RestController 어노테이션을 통해 요청을 매핑하고 입력 처리 및 예외 처리를 지원한다. 컨트롤러는 메소드 기반으로 동작하며, 상속이나 인터페이스 구현 없이도 다양한 방식으로 활용할 수 있다.

Annotated Controllers

설명

스프링 WebFlux는 어노테이션 기반 프로그램밍 모델을 지원하기 때문에 @Controller, @RestController 컴포넌트로 요청을 매핑하고, 입력을 받고, 예외처리를 할수 있다. 컨트롤러는 메소드를 여러가지로 활용할 수 있어서 클래스를 상속하거나 인터페이스를 구현하지 않아도 된다.

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String handle() {
        return "Hello WebFlux";
    }
}

위 코드에서는 response body에 쓸 String을 리턴한다.

@Controller

컨트롤러는 표준 스프링 빈으로 정의한다. @Controller 어노테이션을 달면 스프링이 클래스패스 내의 다른 @Component 클래스처럼 자동으로 스캔하고 빈으로 등록한다. 이 어노테이션을 선언하면 그 클래스가 Web 컴포넌트라는 뜻이기도 하다. @Controller 빈을 자동으로 등록하려면 아래와 같이 컴포넌트 스캔을 위한 설정이 필요하다.

@Configuration
@ComponentScan("org.example.web")
public class WebConfig {
// ...
}

@RestController는 자체에 @Controller, @ResponseBody를 선언하고 있어, 컨트롤러 내의 모든 메소드에 @ResponseBody를 상속한다. 따라서 리턴값으로 View를 만들지 않고 response body에 바로 쓸 수 있다.

Request Mapping

컨트롤러 메소드 요청을 매핑할 때는 @RequestMapping을 사용한다. 이 어노테이션에 있는 속성으로 URL, HTTP 메소드, 요청 파라미터, 헤더, 미디어 타입을 매칭할 수 있다. 메소드에 선언하거나 모든 메소드에서 공유하고 싶을 때 클래스 레벨에 선언한다. @GetMapping, @PostMapping, @PutMapping, @DeleteMapping, @PatchMapping은 HTTP 메소드를 바로 지정할 수 있다. 이 어노테이션은 컨트롤러 메소드에서 거의 대부분이 HTTP 메소드 하나만 담당하는 일종의 커스텀 어노테이션이다. 이 어노테이션을 선언하더라도 다른 매핑 조건을 공통으로 사용하려면 클래스 레벨의 @RequestMapping을 선언해야 한다.

Handler Methods

@RequestMapping 핸들러는 다양한 컨트롤러 메소드 인자와 리턴값을 지원하므로 원하는 것을 선택하면 된다. 블로킹 I/O로 받는 인자(예를 들어 response body를 읽는 경우)는 리액티브 유형(Reactor, RxJava 또는 기타)을 사용할 수 있다. 이런 타입은 Description 컬럼에 명시되어 있다. 블로킹 없는 인자는 리액티브 타입을 사용하지 않는다. 일부 어노테이션은(예를 들어 @RequestParam, @RequestHeader 등) required attribute로 필수 여부를 지정할 수 있으며 JDK 8의 java.util.Optional을 사용해도 된다. 효과는 required=false와 동일하다.

Model

@ModelAttribute 어노테이션은 다음과 같이 사용할 수 있다.

  • @RequestMapping 메소드 인자에 선언해서 모델을 생성, 접근하고 WebDataBinder로 객체에 바인딩한다.
  • @Controller나 @ControllerAdvice 클래스 메소드에 선언해서 다른 @RequestMapping 메소드를 실행하기 전 모델을 초기화한다.
  • @RequestMapping 메소드에서 리턴하는 값을 molde attribute로 만든다.

여기서는 @ModelAttribute 메소드에 대해 설명한다. 컨트롤러는 @ModelAttribute 메소드를 얼마든지 가질 수 있다. 이러한 모든 메소드는 동일한 컨트롤러에서 @RequestMapping 메소드 앞에 호출된다. @ModelAttribute 메소드는 @ControllerAdvice를 통해 컨트롤러 간에 공유할 수도 있다.

@ModelAttribute 메소드는 여러가지 방법으로 활용할 수 있다. 지원하는 인자는 대부분 @RequestMapping 메소드와 동일하다.(@ModelAttribute 자체와 request body와 관련된 것은 제외) 다음은 @ModelAttribute 메소드 사용 예제이다.

@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}

DataBinder

WebDataBinder는 @Controller나 @ControllerAdvice 클래스에서 @InitBinder 메소드로 초기화할 수 있다. @InitBinder 메소드는 다음과 같이 사용할 수 있다.

@InitBinder 메소드롤 컨트롤러별 java.beans.PropertyEditor나 스프링 Converter, Formatter를 등록할 수 있다. FormattingConversionService에서 전역으로 사용하는 Converter, Formatter는 웹플럭스 설정으로 등록한다. @InitBinder 메소드가 지원하는 인자는 @ModelAttribute(커맨드 객체)만 제외하고 대부분 @RequestMapping 메소드와 동일하다. 보통은 WebDataBinder를 인자로 받아 컴포넌트를 등록하고 void를 리턴한다. 다음은 @InitBinder 어노테이션을 사용하는 예제이다.

@Controller
public class FormController {

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    }
 
    // ...
}

Managing Exceptions

@Controller와 @ControllerAdvice 클래스 메소드에 @ExceptionHandler를 선언하면 컨트롤러 메서드의 예외를 처리할 수 있다. 다음은 예외를 처리하는 예제이다.

@Controller
public class SimpleController {

    // ...
 
    @ExceptionHandler
    public ResponseEntity<String> handle(IOException ex) {
        // ...
    }
}

Controller Advice

@ExceptionHandler, @InitBinder, @ModelAttribute 메소드는 @Controller 클래스(혹은 상속한 클래스)에 적용된다. 모든 컨트롤러에 적용하고 싶다면 @ControllerAdvice나 @RestControllerAdvice를 선언한 클래스 안에 만들어야 한다.

@ControllerAdvice는 @Component 어노테이션이 선언되어 있기 때문에 컴포넌트 스캔으로 스프링 빈에 등록할 수 있다. @RestControllerAdvice는 @ControllerAdvice와 @ResponseBody가 둘 다 선언되어 있어 @ExceptionHandler 메소드에서 리턴한 값은 메시지 변환을 통해 view를 만들거나 템플릿을 랜더링하는 대신 response body로 렌더링한다.

애플리케이션을 기동하면 프레임워크 내부에서 @ControllerAdvice를 선언한 스프링 빈을 찾아 @RequestMapping과 @ExceptionHandler 메소드를 적용한다. 전역에 설정한 @ExceptionHandler 메소드는 @Controller 메소드 다음에 적용한다. 반대로 전역 @ModelAttribute, @InitBinder 메소드는 @Controller 메소드 전에 적용한다.

기본적으로 @ControllerAdvice 메소드는 모든 요청에 적용되지만 다음 예제처럼 어노테이션 attribute로 컨트롤러를 지정할 수 있다.

// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

참고자료

3.16 - 스프링 WebFlux 함수형 프로그래밍 모델(WebFlux.fn)

스프링 WebFlux의 함수형 모델인 WebFlux.fn은 요청을 함수로 라우팅하고 처리하며, HandlerFunction이 HTTP 요청을 처리해 비동기 응답을 반환한다. RouterFunction은 요청을 해당 HandlerFunction에 매핑하며, 이는 어노테이션 모델의 @RequestMapping과 같은 역할을 수행하지만 데이터와 행동을 함께 제공하는 점이 다르다.

Functional Endpoints

스프링 WebFlux는 경량화 된 함수형 프로그래밍 모델을 지원한다. WebFlux.fn이라고 하는 이 모델은 함수로 요청을 라우팅하고 핸들링하기 때문에 불변성(immutability)을 보장한다. 함수형 모델과 어노테이션 모델 중 하나를 선택하면 되는데 둘 다 리액티브 코어를 기반으로 한다.

설명

WebFlux.fn에선 HandlerFunction이 HTTP 요청을 처리한다. HandlerFunction은 ServerRequest를 받아 비동기 ServerResponse(예를 들어 Mono)를 리턴하는 함수다. 요청, 응답 객체 모두 불변하기 때문에 JDK 8 방식으로 HTTP 요청, 응답에 접근할 수 있다. HandlerFunction 역할은 어노테이션 프로그래밍 모델로 치면 @RequestMapping 메소드와 동일하다.

요청은 RouterFunction이 핸들러 펑션에 라우팅한다. RouterFunction은 ServerRequest를 받아 비동기 HandlerFunction(예를 들어 Mono)을 리턴하는 함수다. 매칭되는 라이터 펑션이 있으면 핸들러 펑션을 리턴하고 그 외는 비어 있는 Mono를 리턴한다. RouterFunction이 하는 일은 @RequestMapping 어노테이션과 동일하지만 라우터 펑션은 테이터뿐 아니라 행동까지 제공한다는 점이 다르다.

라이터를 만들 때는 아래 예제처럼 RouterFunctions.route()가 제공하는 빌더를 사용할 수 있다.

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.build();

public class PersonHandler {
// ...

    public Mono<ServerResponse> listPeople(ServerRequest request) {
        // ...
    }
 
    public Mono<ServerResponse> createPerson(ServerRequest request) {
        // ...
    }
 
    public Mono<ServerResponse> getPerson(ServerRequest request) {
        // ...
    }
}

RouterFunction을 실행하는 방법 중 하나는 HttpHandler로 변환해 내장된 서버 어댑터에 등록하는 것이다.

  • RouterFunctions.toHttpHandler(RouterFunction)
  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)

HandlerFunction

ServerRequest와 ServerResponse는 JDK 8 방식으로 HTTP 요청과 응답에 접근할 수 있는 불변(immutable) 인터페이스이다. 모든 요청, 응답 body 모두 리액티브 스트림 back pressure로 처리한다. request body는 리액터 Flux나 Mono로 표현한다. response body는 Flux와 Mono를 포함한 어떤 리액티브 스트림 Publisher든 상관없다.

ServerRequest

ServerRequest로 HTTP 메소드, URI, 헤더, 쿼리 파라미터에 접근할 수 있으며, body를 추출할 수 있는 메소드를 제공한다. 다음은 request body를 Mono으로 추출하는 예제다.

Mono<String> string = request.bodyToMono(String.class);

다음 예제는 body를 Flux으로 추출한다. Person 객체는 JSON이나 XML 같은 직렬화된 데이터로 디코딩한다.

Flux<Person> people = request.bodyToFlux(Person.class);

위 예제에서 사용한 메소드는 함수형 인터페이스 BodyExtractor를 받는 ServerRequest.body(BodyExtractor) 메소드의 축약 버전이다. BodyExtractors 유틸리티 클래스에 있는 인터페이스를 활용해도 된다. 예를 들면 앞의 예제는 아래와 같이 작성할 수도 있다.

Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));

아래 예제는 form 데이터에 접근하는 방법을 보여준다.

Mono<MultiValueMap<String, String>> map = request.formData();

다음은 multipart 데이터를 map으로 가져오는 예제이다.

Mono<MultiValueMap<String, Part>> map = request.multipartData();

다음은 multipart 데이터를 스트리밍 방식으로 한번에 하나씩 가져온다.

Flux<Part> parts = request.body(BodyExtractors.toParts());

ServerResponse

HTTP 응답은 ServerResponse로 접근할 수 있으며 이 인터페이션은 불변이기 때문에 build 메소드로 생성한다. 빌더로 헤더를 추가하거나 상태코드, body를 설정할 수 있다. 다음은 JSON 컨텐츠로 200(OK) 응답을 만드는 예제이다.

Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);

다음 예제는 boby 없이 Location 헤더만 201(CREATED) 응답을 만든다.

URI location = ...
ServerResponse.created(location).build();

hint 파라미터를 넘기면 사용하는 코덱에 따라 body 직렬화/역직렬화 방식을 커스텀 할 수 있다. 아래 예제처럼 Jackson JSON View를 지정할 수 있다.

ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);

Handler Classes

핸들러 평션은 다음처럼 람다로 만들 수 있다.

HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().bodyValue("Hello World");

편리한 방식이긴 하지만 펑션을 여러 개 사용해야 한다면 인라인 람다로 만들기는 부담스럽다. 이럴 때는 핸들러 클래스로 관련 핸들러 펑션을 묶을 수 있다. 핸들러 클래스는 어노테이션 기반 애플리케이션의 @Controller와 비슷하다. 다음 예제는 리액티브 Person 레퍼지토리와 관련된 요청을 처리한다.

import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

public class PersonHandler {
private final PersonRepository repository;

    public PersonHandler(PersonRepository repository) {
        this.repository = repository;
    }
 
    public Mono<ServerResponse> listPeople(ServerRequest request) {
        Flux<Person> people = repository.allPeople();
        return ok().contentType(APPLICATION_JSON).body(people, Person.class);
    }
 
    public Mono<ServerResponse> createPerson(ServerRequest request) {
        Mono<Person> person = request.bodyToMono(Person.class);
        return ok().build(repository.savePerson(person));
    }
 
    public Mono<ServerResponse> getPerson(ServerRequest request) {
        int personId = Integer.valueOf(request.pathVariable("id"));
        return repository.getPerson(personId)
            .flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
            .switchIfEmpty(ServerResponse.notFound().build());
    }
}

Validation

함수형 프로그래밍 모델은 스프링 validation facilities를 사용해서 request body를 검증할 수 있다. 다음 예제는 Person에 대한 커스텀 스프링 Validator 구현체를 보여주고 있다.

public class PersonHandler {
private final Validator validator = new PersonValidator();

    // ...
 
    public Mono<ServerResponse> createPerson(ServerRequest request) {
        Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate);
        return ok().build(repository.savePerson(person));
    }
 
    private void validate(Person person) {
        Errors errors = new BeanPropertyBindingResult(person, "person");
        validator.validate(person, errors);
        if (errors.hasErrors()) {
            throw new ServerWebInputException(errors.toString());
        }
    }
}

RouterFunction

라우터 펑션은 요청을 그에 맞는 HandlerFunction으로 라우팅한다. 라우팅 펑션을 직접 만들기보단, 보통 RouterFunctions 유틸리티 클래스를 사용한다. RouterFunctions.route()가 리턴하는 빌더를 사용하거나 RouterFunctions.route(RequestPredicate, HandlerFunction)으로 직접 라우터를 만들 수 있다. route() 빌더를 사용하면 static 메소드를 직접 임포트하지 않아도 된다. 예를 들어 GET 요청을 매핑할 수 있는 GET(String, HandlerFunction) 메소드와 POST 요청을 매핑하는 POST(String, HandlerFunction) 메소드가 있다. 빌더는 HTTP 메소드 외에 다른 조건으로 요청을 매핑할 수는 인터페이스도 제공한다. 각 HTTP 메소드는 RequestPredicate 파라미터를 받은 메소드를 오버로딩하고 있기 때문에 다른 조건을 추가할 수 있다.

Predicates

RequestPredicate를 직접 만들어도 되지만 요청 path, HTTP 메소드, 컨텐츠 타입 등 자주 사용하는 구현체는 RequestPredicates 유틸리티 클래스에 준비되어 있다. 다음은 유틸리티 클래스로 Accept 헤더 조건을 추가하는 예제이다.

RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().bodyValue("Hello World")).build();

여러 조건을 함께 사용할 수 있다.

  • RequestPredicate.and(RequestPredicate) - 둘 다 만족해야 한다.
  • RequestPredicate.or(RequestPredicate) - 둘 중 하나만 만족하면 된다. RequestPredicates가 제공하는 구현체도 이 조합으로 만든 것이 많다. 예를 들어 RequestPredicates.GET(String)은 RequestPredicates.method(HttpMethod)와 RequestPredicates.path(String) 조합이다. 위에 있는 예제도 빌더 내부에서 RequestPredicates.GET과 accept를 조합한 것이다.

Routes

라우터 평션은 정해진 순서대로 실행한다. 첫번째 조건과 일치하지 않으면 두번째를 실행하는 식이다. 따라서 구체적인 조건을 앞에 선언해야 한다. 어노테이션 프로그래밍 모델에선 자동으로 가장 구체적인 컨트롤러 메소드를 실행하지만 함수형 모델에서는 그렇지 않다는 점을 유의해야 한다. build()를 호출하면 빌더에 정의한 모든 라우터 펑션을 RouterFunction 한 개로 합친다. 다음 방법으로도 여러 라우터 펑션을 조합할 수 있다.

  • RouterFunctions.route() 빌더의 add(RouterFunction)
  • RouterFunction.and(RouterFunction)
  • RouterFunction.andRoute(RequestPredicate, HandlerFunction) - RouterFunctions.route()를 RouterFunction.and()로 감싸고 있는 축약 버전 다음 예제는 라우터 펑션 4개를 사용한다.
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> otherRoute = ...

RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET("/person", accept(APPLICATION_JSON), handler::listPeople)
.POST("/person", handler::createPerson)
.add(otherRoute)
.build();

Nested Routes

path가 같으면 대부분 같은 조건으로 사용하므로 라우터 평션을 그룹핑하는 경우가 많다. 앞의 예제는 라우터 펑션 세 개가 /person을 path 조건으로 사용했다. 어노테이션을 사용했다면 클래스 레벨이 @RequestMapping을 선언해 중복 코드를 줄일 수 있다. WebFlux.fn에선 path 메소드로 path 조건을 공유한다. 예를 들어 위 코드는 아래 예제처럼 라우터 펑션을 한번 감싸 개선할 수 있다.

RouterFunction<ServerResponse> route = route()
.path("/person", builder -> builder
.GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
.GET(accept(APPLICATION_JSON), handler::listPeople)
.POST(handler::createPerson))
.build();

path가 가장 흔하기 하지만 빌더의 nest 메소드는 다른 조건도 감쌀 수 있다. 위 코드는 여전히 Accpet 헤더가 중복이다 nest 메소드를 함께 사용하면 코드를 한층 더 개선할 수 있다.

RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.build();

Running a Server

HTTP 서버에선 어떻게 라우터 펑션을 실행할까? 간단하게는 다음과 같이 라우터 펑션을 HttpHandler로 변환할 수 있다.

  • RouterFunctions.toHttpHandler(RouterFunction)
  • RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies) 리턴 받은 HttpHandler를 서버 가이드에 따라 서버 어댑터와 함께 사용하면 된다.

스프링 부트에서도 사용하는 좀 더 일반적인 옵션은 WebFlux Config로 컴포넌트를 스프링 빈으로 정의하고 DispatcherHandler와 함께 실행하는 것이다. 프레임워크는 다음과 같은 컴포넌트로 함수형 엔드포인트를 지원하는데 웹플럭스 설정을 사용하면 이를 모두 스프링 빈으로 정의한다.

  • RouterFunctionMapping: 스프링 설정에서 RouterFunction을 찾아 RouterFunction.andOther로 연결하고 최종 구성한 RouterFunction으로 요청을 라우팅한다.
  • HandlerFunctionAdapter: 요청에 매핑된 HandlerFunction을 DispatcherHandler가 실행하게 도와주는 간단한 어댑터
  • ServerResponseResultHandler: ServerResponse의 writeTo 메소드로 HandlerFunction 결과를 처리한다. 위 컴포넌트가 함수형 엔드포인트를 DispatcherHandler의 요청 처리 패턴에 맞춰주기 때문에 어노테이션 컨트롤러와 함께 사용할 수도 있다. 스프링 부트 웹플럭스 스타터도 이 방법으로 함수형 엔드포인트를 지원한다.

Filtering Handler Functions

핸들러 펑션에 필터를 적용할 땐 라우터 빌더의 before, after, filter 메소드를 사용한다. 이 기능을 어노테이션 모델로 구현하면 @ControllerAdvice나 ServletFilter를 사용했을 것이다. 필터는 빌더의 모든 라우터 펑션에 적용된다. 이 말은 필터를 감싸져 있는 라우터에서 정의하면 상위 레벨에는 적용되지 않는다는 뜻이다.

RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET(handler::listPeople)
            .before(request -> ServerRequest.from(request)
                .header("X-RequestHeader", "Value")
                .build()))
        .POST(handler::createPerson))
    .after((request, response) -> logResponse(response))
    .build();

라우터 빌더의 필터 메서드는 서버 요청과 핸들러 함수를 받아 서버 응답을 반환하는 함수인 핸들러 필터 함수를 받는다. 핸들러 함수 매개변수는 체인의 다음 요소를 나타낸다. 일반적으로 라우팅되는 핸들러이지만 여러 개의 필터가 적용되는 경우 다른 필터가 될 수도 있다. 이제 path를 보고 요청을 허가할지 말지를 결정하는 SecurityManager가 있다고 가정하고 간단한 보안 필터를 라우터에 적용해 보자.

SecurityManager securityManager = ...

RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET(handler::listPeople))
        .POST(handler::createPerson))
    .filter((request, next) -> {
        if (securityManager.allowAccessTo(request.path())) {
            return next.handle(request);
        }
        else {
            return ServerResponse.status(UNAUTHORIZED).build();
        }
    })
    .build();

위 예제를 보면 next.handle(ServerRequest) 호출은 선택이라는 점을 알 수 있다. 여기선 접근을 허가할 때만 실행했다. 빌더의 filter 메소드 대신 RouterFunction.filter(HandlerFilterFunction)로 필터를 추가하는 방법도 있다.

함수형 엔드포인트에서 CORS는 CorsWebFilter로 지원한다.

참고자료

3.17 - 스프링 WebFlux의 WebClient와 논블로킹 처리

스프링 WebFlux는 리액티브, 논블로킹 HTTP 요청을 위한 WebClient를 제공하며, 이를 통해 선언적인 프로그래밍이 가능하다. WebClient와 서버는 동일한 논블로킹 코덱을 사용해 요청과 응답을 인코딩 및 디코딩한다.

WebClient

스프링 WebFlux는 리액티브, 논블로킹 HTTP 요청을 위한 WebClient를 제공한다. 웹 클라이언트는 리액티브 타입을 사용하는 함수형 API이기 때문에 선언적인(declarative) 프로그래밍이 가능하다. 웹플럭스 클라이언트와 서버는 동일한 논블로킹 코덱으로 요청, 응답을 인코딩, 디코딩한다.

설명

WebClient는 요청을 수행하기 위해 HTTP 클라이언트 라이브러리에 처리를 위임하며 아래와 같은 기능을 기본으로 제공한다.

  • Reactor Netty
  • Jetty Reactive HttpClient
  • Apache HttpComponents
  • Others can be plugged via ClientHttpConnector.

Configuration

WebClient는 가장 간단하게는 스태틱 팩토리 메소드로 만들 수 있다.

  • WebClient.create()
  • WebClient.create(String baseUrl)

위 메소드는 디폴트 세팅으로 Reactor Netty HttpClient를 사용하므로 클래스패스에 io.projectreactor.netty:reactor-netty가 있어야 한다.

다른 옵션을 사용하려면 WebClient.builder()를 사용한다.

  • uriBuilderFactory: base URL을 커스텀한 UriBuilderFactory
  • defaultHeader: 모든 요청에 사용할 헤더
  • defaultCookie: 모든 요청에 사용할 쿠키
  • defaultRequest: 모든 요청을 커스텀할 Consumer
  • filter: 모든 요청에 사용할 클라이언트 필터
  • exchangeStrategies: HTTP 메시지 reader/writer 커스텀
  • clientConnector: HTTP 클라이언트 라이브러리 설정

다음 예제는 HTTP 코덱을 설정한다.

WebClient client = WebClient.builder()
    .codecs(configurer -> ... )
    .build();

WebClient는 한 번 빌드하고 나면 상태를 변경할 수 없다. 단 다음 예제와 같이 원본 인스턴스는 그대로 두고 복사해와서 설정을 추가할 수 있다.

WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build();

WebClient client2 = client1.mutate()
.filter(filterC).filter(filterD).build();

// client1 has filterA, filterB

// client2 has filterA, filterB, filterC, filterD

MaxInMemorySize

스프링 WebFlux는 애플리케이션 메모리 이슈를 방지하기 위해 코덱의 메모리 버퍼 사이즈를 제한한다. 디폴트는 256KB로 설정되어 있는데 버퍼가 부족하면 다음과 같은 에러가 보인다.

org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer

다음 코드를 사용하면 모든 디폴트 코덱의 최대 버퍼 사이즈를 조절할 수 있다.

WebClient webClient = WebClient.builder()
    .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
    .build();

Reactor Netty

HttpClient는 Reactor Netty 설정을 커스텀할 수 있는 간단한 설정 프리셋을 가지고 있다.

HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);

WebClient webClient = WebClient.builder()
    .clientConnector(new ReactorClientHttpConnector(httpClient))
    .build();

Resources

기본적으로 HttpClient는 reactor.netty.http.HttpResources에 묶여 있는 Reactor Netty의 글로벌 리소스를 사용한다. 이는 이벤트 루프 쓰레드와 커넥션 풀도 포함한다.
이벤트 루프로 동시성을 제어하려면 공유 리소스를 고정해 놓고 사용하는게 좋기 때문이다. 이 모드에서는 프로세스가 종료될 때까지 공유 자원을 active 상태로 유지한다.
서버가 프로세스와 함께 중단된다면 명시적으로 리소스를 종료시킬 필요는 없다. 하지만 프로세스 내에서 서버를 시작하거나 중단할 수 있다면(예를 들어 WAR로 배포한 스프링 MVC 애플리케이션) 다음 예제처럼 스프링이 관리하는 ReactorResourceFactory 빈을 globalResources=true롤 선언해야 스프링 ApplicationContext를 닫을 때 Reactor Netty 글러벌 리소스도 종료한다.

@Bean
public ReactorResourceFactory reactorResourceFactory() {
return new ReactorResourceFactory();
}

원한다면 글로벌 Reactor Netty 리소스를 사용하지 않게 만들수도 있다. 하지만 아래 예제처럼 직접 모든 Reactor Netty 클라이언트와 서버 인스턴스가 공유 자원을 사용하게 만들어야 한다.

@Bean
public ReactorResourceFactory resourceFactory() {
    ReactorResourceFactory factory = new ReactorResourceFactory();
    factory.setUseGlobalResources(false);
    return factory;
}

@Bean
public WebClient webClient() {

    Function<HttpClient, HttpClient> mapper = client -> {
        // Further customizations...
    };
 
    ClientHttpConnector connector =
            new ReactorClientHttpConnector(resourceFactory(), mapper);
 
    return WebClient.builder().clientConnector(connector).build();
}

Timeouts

다음은 커넥션 타임아웃을 설정하는 코드다.

import io.netty.channel.ChannelOption;

HttpClient httpClient = HttpClient.create()
    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);

WebClient webClient = WebClient.builder()
    .clientConnector(new ReactorClientHttpConnector(httpClient))
    .build();

다음은 read/write 타임아웃을 설정하는 코드다.

import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;

HttpClient httpClient = HttpClient.create()
    .doOnConnected(conn -> conn
        .addHandlerLast(new ReadTimeoutHandler(10))
        .addHandlerLast(new WriteTimeoutHandler(10)));

// Create WebClient...

다음은 모든 요청에 대한 타임아웃을 설정하는 코드다.

HttpClient httpClient = HttpClient.create()
    .responseTimeout(Duration.ofSeconds(2));

// Create WebClient...

다음은 특정 요청에 타임아웃을 설정하는 코드다.

WebClient.create().get()
    .uri("https://example.org/path")
    .httpRequest(httpRequest -> {
        HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
        reactorRequest.responseTimeout(Duration.ofSeconds(2));
    })
    .retrieve()
    .bodyToMono(String.class);

Jetty

다음은 Jetty HttpClient 설정을 커스텀하는 예제이다.

HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);

WebClient webClient = WebClient.builder()
    .clientConnector(new JettyClientHttpConnector(httpClient))
    .build();

HttpClient는 전용 리소스(Executor. ByteBufferPool, Scheduler)를 생성해서 기본적으로 프로세스가 종료되거나 stop()을 호출할 때까지 유지한다.

다음 예제처럼 스프링이 관리하는 JettyResourceFactory 빈을 정의하면 여러 Jetty 클라이언트(혹은 서버) 인스턴스에서 리소스를 공유할 수 있고 스프링 ApplicationContext를 닫을 때 리소스도 종료시킬 수 있다.

@Bean
public JettyResourceFactory resourceFactory() {
    return new JettyResourceFactory();
}

@Bean
public WebClient webClient() {

    HttpClient httpClient = new HttpClient();
    // Further customizations...
 
    ClientHttpConnector connector =
            new JettyClientHttpConnector(httpClient, resourceFactory());
 
    return WebClient.builder().clientConnector(connector).build();
}

HttpComponents

다음은 Apache HttpComponents HttpClient 설정을 커스텀하는 예제이다.

HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);

WebClient webClient = WebClient.builder().clientConnector(connector).build();

retrieve()

retrieve()는 response body를 받아 디코딩하는 간단한 메소드이다. 사용방법은 아래와 같다.

WebClient client = WebClient.create("https://example.org");

Mono<ResponseEntity<Person>> result = client.get()
    .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .toEntity(Person.class);

혹은 body만 받아올 수 있다.

WebClient client = WebClient.create("https://example.org");

Mono<Person> result = client.get()
    .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .bodyToMono(Person.class);

다음 예제처럼 응답을 객체 스트림으로도 디코딩할 수 있다.

Flux<Quote> result = client.get()
    .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
    .retrieve()
    .bodyToFlux(Quote.class);

4xx 또는 5xx 응답코드를 받으면 디폴트는 WebClientResponseException 또는 각 HTTP 상태에 해당하는 WebClientResponseException.BadRequest, WebClientResponseException.NotFound 등의 하위 exception을 던진다. 다음 예제처럼 onStatus 메소드로 상태별 exception을 커스텀할 수 있다.

Mono<Person> result = client.get()
    .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
    .retrieve()
    .onStatus(HttpStatus::is4xxClientError, response -> ...)
    .onStatus(HttpStatus::is5xxServerError, response -> ...)
    .bodyToMono(Person.class);

Exchange

exchangeToMono() 및 exchangeToFlux() 메서드(또는 Kotlin의 awaitExchange {} 및 exchangeToFlow {})는 응답 상태에 따라 응답을 다르게 디코딩하는 등 더 많은 제어가 필요한 곳에 유용하다.

Mono<Person> entityMono = client.get()
    .uri("/persons/1")
    .accept(MediaType.APPLICATION_JSON)
    .exchangeToMono(response -> {
        if (response.statusCode().equals(HttpStatus.OK)) {
            return response.bodyToMono(Person.class);
        }
        else {
            // Turn to error
            return response.createException().flatMap(Mono::error);
        }
    });

위의 방법을 사용할 때는 반환된 Mono 또는 Flux가 완료된 후 응답 본문을 확인하고 소비되지 않은 경우 메모리 및 연결 누수를 방지하기 위해 해제한다. 따라서 응답은 더 이상 다운스트림에서 디코딩할 수 없다. 필요한 경우 응답을 디코딩하는 방법을 선언하는 것은 제공된 함수에 달려 있다.

Request Body

request body는 Mono, 코틀린 코루틴 Deferred 등 ReactiveAdapterRegistry에 등록된 모든 비동기 타입으로 인코딩할 수 있다.

Mono<Person> personMono = ... ;

Mono<Void> result = client.post()
    .uri("/persons/{id}", id)
    .contentType(MediaType.APPLICATION_JSON)
    .body(personMono, Person.class)
    .retrieve()
    .bodyToMono(Void.class);

다음 예제처럼 객체 스트림으로도 인코딩할 수 있다.

Flux<Person> personFlux = ... ;

Mono<Void> result = client.post()
    .uri("/persons/{id}", id)
    .contentType(MediaType.APPLICATION_STREAM_JSON)
    .body(personFlux, Person.class)
    .retrieve()
    .bodyToMono(Void.class);

비동기 타입이 아닌 실제 값을 가지고 있다면 bodyValue를 사용한다.

Person person = ... ;

Mono<Void> result = client.post()
    .uri("/persons/{id}", id)
    .contentType(MediaType.APPLICATION_JSON)
    .bodyValue(person)
    .retrieve()
    .bodyToMono(Void.class);

Form Data

form 데이터를 보내려면 MultiValueMap<String, String>을 body로 사용해야 한다. 이 때는 FormHttpMessageWriter가 자동으로 content-type을 application/x-www-form-urlencoded로 설정한다. 다음은 MultiValueMap<String, String>을 사용하는 예제이다.

MultiValueMap<String, String> formData = ... ;

Mono<Void> result = client.post()
    .uri("/path", id)
    .bodyValue(formData)
    .retrieve()
    .bodyToMono(Void.class);

BodyInserters를 사용하면 인라인으로 form 데이터를 만들 수 있다.

import static org.springframework.web.reactive.function.BodyInserters.*;

Mono<Void> result = client.post()
    .uri("/path", id)
    .body(fromFormData("k1", "v1").with("k2", "v2"))
    .retrieve()
    .bodyToMono(Void.class);

Multipart Data

multipart 데이터를 보낼 때는 MultiValueMap<String, ?>을 사용해서 각 value에 part 컨텐츠를 나타내는 Object 인스턴스나 part의 컨텐츠와 헤더를 나타내는 HttpEntity를 담아야 한다. MultipartBodyBuilder를 사용하면 편리하다.

MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
builder.part("filePart1", new FileSystemResource("...logo.png"));
builder.part("jsonPart", new Person("Jason"));
builder.part("myPart", part); // Part from a server request

MultiValueMap<String, HttpEntity<?>> parts = builder.build();

일반적으로는 part별로 content-type을 명시하지 않아도 된다. Content Type은 직렬화할때 쓰는 HttpMessageWriter나 Resource의 경우 파일 확장자에 따라 자동으로 결정한다. 필요하다면 빌더 part 메소드 중 MediaType을 받는 메소드를 사용하면 된다. MultiValueMap을 만들었다면 가장 간단하게는 다음 예제처럼 body 메소드로 WebClient에 넘길 수 있다.

MultipartBodyBuilder builder = ...;

Mono<Void> result = client.post()
    .uri("/path", id)
    .body(builder.build())
    .retrieve()
    .bodyToMono(Void.class);

MultiValueMap에 전형적인 form 데이터(application/x-www-form-urlencoded) 등 String이 아닌 갓이 하나라도 들어있으면 content-type을 multipart/form-data로 설정하지 않아도 된다. MultipartBodyBuilder를 사용하면 항상 HttpEntity로 감싸주면 된다. MultipartBodyBuilder 대신 BodyInserters를 사용하면 인라인으로 multipart 컨텐츠를 만들 수 있다.

import static org.springframework.web.reactive.function.BodyInserters.*;

Mono<Void> result = client.post()
    .uri("/path", id)
    .body(fromMultipartData("fieldPart", "value").with("filePart", resource))
    .retrieve()
    .bodyToMono(Void.class);

Filter

다음 예제와 같이 WebClient.Builder를 통해 클라이언트 필터(ExchangeFilterFunction)를 등록하여 요청을 가로채고 수정할 수 있다.

WebClient client = WebClient.builder()
.filter((request, next) -> {

            ClientRequest filtered = ClientRequest.from(request)
                    .header("foo", "bar")
                    .build();
 
            return next.exchange(filtered);
        })
        .build();

필터는 인증과 같은 교차 문제에 사용할 수 있다. 다음 예제에서는 정적 팩토리 메서드를 통한 기본 인증에 필터를 사용한다.

import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;

WebClient client = WebClient.builder()
    .filter(basicAuthentication("user", "password"))
    .build();

필터는 기존 웹클라이언트 인스턴스를 변경하여 추가하거나 제거할 수 있으므로 원래 웹클라이언트에 영향을 주지 않는 새 웹클라이언트 인스턴스를 생성할 수 있다.

import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;

WebClient client = webClient.mutate()
    .filters(filterList -> {
        filterList.add(0, basicAuthentication("user", "password"));
    })
    .build();

WebClient는 일련의 필터 체인을 둘러싸고 있는 얇은 외피에 ExchangeFunction이 뒤따른다. 요청을 하고 상위 수준 객체와 인코딩하기 위한 워크플로우를 제공하며 응답 콘텐츠가 항상 소비되도록 하는 데 도움이 된다.
필터가 어떤 방식으로든 응답을 처리할 때는 항상 콘텐츠를 소비하거나 웹클라이언트로 다운스트림으로 전파하여 동일한 콘텐츠를 보장할 수 있도록 각별한 주의를 기울여야 한다.
다음 예제는 미승인 상태 코드를 처리하지만 예상 여부에 관계없이 모든 응답 콘텐츠가 공개되도록 하는 필터다.

public ExchangeFilterFunction renewTokenFilter() {
    return (request, next) -> next.exchange(request).flatMap(response -> {
        if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
            return response.releaseBody()
                .then(renewToken())
                .flatMap(token -> {
                    ClientRequest newRequest = ClientRequest.from(request).build();
                    return next.exchange(newRequest);
                });
        } else {
            return Mono.just(response);
        }
    });
}

Attributes

요청에 속성을 추가할 수 있다. 이 기능은 필터 체인을 통해 정보를 전달하고 특정 요청에 대한 필터의 동작에 영향을 주고자 할 때 편리하다.

WebClient client = WebClient.builder()
    .filter((request, next) -> {
        Optional<Object> usr = request.attribute("myAttribute");
        // ...
    })
    .build();

client.get().uri("https://example.org/")
        .attribute("myAttribute", "...")
        .retrieve()
        .bodyToMono(Void.class);

    }

모든 요청에 속성을 삽입할 수 있는 WebClient.Builder 수준에서 전역적으로 defaultRequest 콜백을 구성할 수 있으며, 이는 예를 들어 Spring MVC 애플리케이션에서 ThreadLocal 데이터를 기반으로 요청 속성을 채우는 데 사용될 수 있다.

Context

속성은 필터 체인에 정보를 전달하는 편리한 방법을 제공하지만 현재 요청에만 영향을 준다. 중첩된 추가 요청에 전파되는 정보를 전달하려면(예를 들어 flatMap을 통해), 또는 이후에 실행되는 요청에 전파되는 정보를 전달하려면(예를 들어 concatMap을 통해) Reactor Context를 사용해야 한다.

다음 예제는 리액터 컨텍스트를 리액티브 체인의 끝에 채워 모든 작업에 적용하는 것을 보여준다.

WebClient client = WebClient.builder()
    .filter((request, next) ->
        Mono.deferContextual(contextView -> {
            String value = contextView.get("foo");
            // ...
    }))
    .build();

client.get().uri("https://example.org/")
    .retrieve()
    .bodyToMono(String.class)
    .flatMap(body -> {
        // perform nested request (context propagates automatically)...
    })
    .contextWrite(context -> context.put("foo", ...));

Synchronous Use

WebClient는 마지막에 결과를 블로킹하면 동기(synchronous)로 결과를 가져온다.

Person person = client.get().uri("/person/{id}", i).retrieve()
    .bodyToMono(Person.class)
    .block();

List<Person> persons = client.get().uri("/persons").retrieve()
    .bodyToFlux(Person.class)
    .collectList()
    .block();

하지만 API를 여러번 호출하면 각 응답을 따로 블로킹하기보다는 전체 결과를 합쳐서 기다리는게 더 효율적이다.

Mono<Person> personMono = client.get().uri("/person/{id}", personId)
    .retrieve().bodyToMono(Person.class);

Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
    .retrieve().bodyToFlux(Hobby.class).collectList();

Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
        Map<String, String> map = new LinkedHashMap<>();
        map.put("person", person);
        map.put("hobbies", hobbies);
        return map;
    })
    .block();

위 코드는 단지 한 가지 예시일 뿐이다. 요청이 끝날때까지 블로킹 하지 않고 리액티브 파이프라인을 구축해서 상호독립적으로 원격 호출을 여러번 실행하는 다른 패턴과 연산자도 많다.

스프링 MVC나 WebFlux 컨트롤러에서 Flux나 Mono를 사용한다면 블로킹할 필요가 없다. 단순히 컨트롤러 메소드에서 리액티브 타입을 리턴하기만 하면 된다. 코틀린 코루틴과 스프링 WebFlux에서도 마찬가지다. 컨트롤러 메소드에서 suspend 함수를 사용하거라 Flow를 리턴하면 된다.

참고자료

3.18 - Ajax 지원 서비스

Ajax 지원 서비스는 J2EE 개발자가 쉽게 Ajax 기능을 구현할 수 있도록 AjaxTags 라이브러리를 기반으로 제공하며, 자주 사용하는 기능을 커스텀 태그 형태로 제공한다. 주요 기능으로는 자동완성, 연동된 셀렉트박스, 탭 패널 등이 있으며, 이를 위해 JSP 설정과 Controller에서 AjaxXmlBuilder로 데이터를 가공하여 처리한다.

Ajax 지원 서비스

개요

일반적으로 Ajax 기능은 javascript 언어로 개발하나, server-side 구현에 익숙한 J2EE 개발자들에게는 쉽지 않은 작업이 될 수 있다. Ajax 지원 서비스에서는 Ajax를 이용해 자주 사용되는 기능을 custom tag형태로 제공한다. 기능은 오픈소스 라이브러리인 AjaxTags를 이용한다.

설명

설치

시스템 환경 및 필요 라이브러리

설치 순서

  1. AjaxTags Download 사이트에 가서 해당 라이브러리를 download한 후 WEB-INF/lib에 위치시킨다.
  2. web.xml 설정.
<servlet>
    <servlet-name>sourceloader</servlet-name>
    <servlet-class>net.sourceforge.ajaxtags.servlets.SourceLoader</servlet-class>

    <init-param>
        <param-name>prefix</param-name>
        <param-value>/ajaxtags</param-value>
    </init-param>
</servlet>

<servlet-mapping>
    <servlet-name>sourceloader</servlet-name>
    <url-pattern>/ajaxtags/js/*</url-pattern>
</servlet-mapping>

<servlet-mapping>
    <servlet-name>sourceloader</servlet-name>
    <url-pattern>/ajaxtags/img/*</url-pattern>
</servlet-mapping>

<servlet-mapping>
    <servlet-name>sourceloader</servlet-name>
    <url-pattern>/ajaxtags/css/*</url-pattern>
</servlet-mapping>

AjaxTags Tag Reference

ajax:autocomplete

자동완성기능. 보통 검색 입력창에 prefix 문자를 입력하면 해당 추천 검색어를 보여주는 방식으로 이용.

파라미터설명필수여부
baseUrl자동완성기능을 위한 결과 데이터를 보내주는 server-side 액션을 위한 URL.yes
source추천 검색어 리스트를 보여줄 텍스트 필드 이름. 입력 필드에 추천 검색리스트를 보여준다면 target과 source를 동일하게 입력한다.yes
target사용자가 입력하는 텍스트 필드 이름.yes
parametersbaseUrl에 추가할 파라미터들.여러개일 경우 comma로 구별한다.yes
className추천 검색리스트에 적용할 CSS 클래스이름yes
indicatorAjax 요청중일때 보여줄 표시.no
minimumCharactersAjax 요청을 위한 최소 입력값.no
preFunctionAjax 요청이 시작되기 전에 동작하는 function 이름.no
postFunctionAjax 요청이 완료된 후에 동작하는 function 이름.no
errorFunctionAjax 요청 error시에 동작하는 function 이름.no

ajax:select

하나의 셀렉트박스에서 값을 변경하면 다른 셀렉트박스에 연관된 값으로 리스트를 구성. Linked SelectBox.

파라미터설명필수여부
baseUrl자동완성기능을 위한 결과 데이터를 보내주는 server-side 액션을 위한 URL.yes
source추천 검색어 리스트를 보여줄 텍스트 필드 이름. 입력 필드에 추천 검색리스트를 보여준다면 target과 source를 동일하게 입력한다.yes
target사용자가 입력하는 텍스트 필드 이름.yes
parametersbaseUrl에 추가할 파라미터들.여러개일 경우 comma로 구별한다.no
eventTypeno
executeOnLoad응답 데이터로 select box를 구성하는 중일때 구성중인지를 별도 표시를 할지 여부.[default=false]no
defaultOptionsAjax 응답값이 없을때 보여줄 기본 리스트. comma로 구별하여 작성한다.no
preFunctionAjax 요청이 시작되기 전에 동작하는 function 이름.no
postFunctionAjax 요청이 완료된 후에 동작하는 function 이름.no
errorFunctionAjax 요청 error시에 동작하는 function 이름.no
parser응답 데이터에 대한 parser.[default=ResponseHtmlParser]no

ajax:tabPanel

탭으로 구성된 페이지들 새로 고침 없이 보여 줄때.

파라미터설명필수여부
idtabPanel의 IDyes
preFunctionAjax 요청이 시작되기 전에 동작하는 function 이름.no
postFunctionAjax 요청이 완료된 후에 동작하는 function 이름.no
errorFunctionAjax 요청 error시에 동작하는 function 이름.no
parser응답 데이터에 대한 parser.[default=ResponseHtmlParser]no

others

이외에도 여러 기능이 있다. AjaxTags의 Tag 레퍼런스 및 사용법은 아래 AjaxTags 사이트에서 확인할 수 있다.

공통적인 개발 작업

AjaxTags의 어떤 태그를 사용하던지, 아래의 작업은 공통적으로 발생한다.

JSP

태그 라이브러리 선언

<%@ taglib prefix="ajax" ri="http://ajaxtags.sourceforge.net/tags/ajaxtags" %>

Javascript, CSS 선언

<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/prototype.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/scriptaculous/scriptaculous.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/overlibmws/overlibmws.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/ajaxtags.js"></script>
<link type="text/css" rel="stylesheet" href="<%=request.getContextPath()%>/ajaxtags/css/ajaxtags.css" />
<link type="text/css" rel="stylesheet" href="<%=request.getContextPath()%>/ajaxtags/css/displaytag.css" />

Controller

AjaxTags를 사용하기 위해서는 결과 데이터가 AjaxTags에서 데이터 형식(XML style)을 갖추어야 한다. 이를 위해 AjaxTags는 AjaxXmlBuilder라는 데이터 가공을 위한 API를 제공한다. 결과 데이터를 AjaxXmlBuilder를 이용해서 변환하는 작업을 View에서 할 수도 있지만, View의 갯수가 기능 단위로 추가될 수도 있으므로, Controller에서 변환한 후에 Model 객체에 담아서 View로 보내고 View는 공통으로 하나를 사용하기를 권한다.

org.ajaxtags.helpers.AjaxXmlBuilder

ajaxXml model에 추가하기

List<Department> deptList = departmentService.getDepartmentList(param);
AjaxXmlBuilder ajaxXmlBuilder = new AjaxXmlBuilder();
for (Iterator iter = deptList.iterator(); iter.hasNext();) {
  Department dept = (Department) iter.next();
  ajaxXmlBuilder.addItem(dept.getDeptname(), dept.getDeptid());
}
model.addObject("ajaxXml",ajaxXmlBuilder.toString());

결과 데이터는 다음과 같다.

<?xml version="1.0" encoding="UTF-8"?>
<ajax-response>
  <response>
    <item>
      <name>점심메뉴기획팀</name>
      <value>1200</value>
    </item>
    <item>
      <name>야근금지역량팀</name>
      <value>1300</value>
    </item>
    ...
  </response>
</ajax-response>

View

JSP 페이지에 프린트되는 일반적인 응답방식이 아니므로, 응답 처리를 위한 공통 View를 만들어야 한다. 결과데이터의 형식(XML)을 응답 객체에 설정한다. Controller에서 보낸 Model객체의 결과데이터를 꺼내 write한다.

package com.easycompany.view;
 
import java.io.PrintWriter;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.view.AbstractView;
 
public class AjaxXmlView extends AbstractView {
 
	@Override
	protected void renderMergedOutputModel(Map model,
			HttpServletRequest request, HttpServletResponse response)
			throws Exception {
 
		response.setContentType("text/xml");
		response.setHeader("Cache-Control", "no-cache");
		response.setCharacterEncoding("UTF-8");
 
		PrintWriter writer = response.getWriter();
		writer.write((String) model.get("ajaxXml"));  //Model Attribute 이름은 공통으로 사용하는 것으로...
		writer.close();
	}
}

예제

ajax:autocomplete

사원 정보 조회 페이지에서, 조회 조건중에 하나인 이름 필드에 자동완성기능(autocomplete)을 적용해 보자. 검색하려는 이름을 입력하기 시작하면, 입력값에 해당하는 prefix를 가진 이름들이 추천 리스트로 나온다.

ajax-autocomplete-sample

JSP

/easycompany/webapp/WEB-INF/jsp/employeelist.jsp

...
<%@ taglib prefix="ajax" uri="http://ajaxtags.sourceforge.net/tags/ajaxtags" %>
...
<!--Ajax Tags-->
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/prototype.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/scriptaculous/scriptaculous.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/overlibmws/overlibmws.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/ajaxtags.js"></script>
<link type="text/css" rel="stylesheet" href="<%=request.getContextPath()%>/ajaxtags/css/ajaxtags.css" />
<link type="text/css" rel="stylesheet" href="<%=request.getContextPath()%>/ajaxtags/css/displaytag.css" />
...
<form:form commandName="searchCriteria" action="/easycompany/employeeList.do">
<table width="50%" border="1">
	<tr>
		<td>사원번호 :  <form:input path="searchEid"/> </td>
		<td>부서번호 : <form:input path="searchDid"/> </td>
		<td>이름 :   <form:input path="searchName"/>
		</td>
		<td><input type="submit" value="검색" onclick="this.disabled=true,this.form.submit();" /></td>
	</tr>	
</table>
</form:form>
 
<ajax:autocomplete 
  baseUrl="${pageContext.request.contextPath}/suggestName.do" 
  source="searchName" 
  target="searchName" 
  className="autocomplete" 
  minimumCharacters="1" />
...

Controller

com.easycompany.controller.annotation.AjaxController

package com.easycompany.controller.annotation;
...
import net.sourceforge.ajaxtags.xml.AjaxXmlBuilder;
import com.easycompany.view.AjaxXmlView;
 
@Controller
public class AjaxController {
 
	@Autowired
	private EmployeeService employeeService;
 
	@Autowired
	private DepartmentService departmentService;
 
	@RequestMapping("/suggestName.do")
	protected ModelAndView suggestName(@RequestParam("searchName") String searchName){
 
		ModelAndView model = new ModelAndView(new AjaxXmlView());
		List<String> nameList = employeeService.getNameListForSuggest(searchName);
 
		AjaxXmlBuilder ajaxXmlBuilder = new AjaxXmlBuilder();
 
		for(String name:nameList){
			ajaxXmlBuilder.addItem(name, name, false);
		}
		model.addObject("ajaxXml",ajaxXmlBuilder.toString());
		return model;
	}
}

한글처리설정

위 예제에서 보면 '/suggestName.do?searchName=김' 같이, 사원 이름 prefix값이 파라미터로 전달되는데, 파라미터 값이 한글인 경우 제대로 처리되기 위해서는, {Tomcat DIR}/conf/server.xml에 인코딩 처리를 해줘야 한다. UTF-8 인코딩을 한다면, <Connector/> 태그에 URIEncoding=“utf-8”을 추가하면 된다.

...
    <Connector port="8080" maxHttpHeaderSize="8192"
               maxThreads="150" minSpareThreads="25" maxSpareThreads="75"
               enableLookups="false" redirectPort="8443" acceptCount="100"
               connectionTimeout="20000" disableUploadTimeout="true" URIEncoding="utf-8"/>
...
    <Connector port="8009" 
               enableLookups="false" redirectPort="8443" protocol="AJP/1.3" URIEncoding="utf-8" />

ajax:select

사원 정보 수정(입력) 페이지에서, 상위 부서 정보 select box에서 한 부서를 선택하면, 하위 부서 정보 select box는 해당 상위 부서에 속한 하위 부서 정보들로 옵션을 구성한다.

ajax-select-sample

JSP

/easycompany/webapp/WEB-INF/jsp/addemployee.jsp, /easycompany/webapp/WEB-INF/jsp/modifyemployee.jsp

...
<%@ taglib prefix="ajax" uri="http://ajaxtags.sourceforge.net/tags/ajaxtags" %>
...
<!--Ajax Tags-->
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/prototype.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/scriptaculous/scriptaculous.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/overlibmws/overlibmws.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/ajaxtags.js"></script>
<link type="text/css" rel="stylesheet" href="<%=request.getContextPath()%>/ajaxtags/css/ajaxtags.css" />
<link type="text/css" rel="stylesheet" href="<%=request.getContextPath()%>/ajaxtags/css/displaytag.css" />
...
<form:form commandName="employee">
...
	<tr>
		<th>부서번호</th>
		<td>
			<form:select path="superdeptid">
				<option value="">상위부서를 선택하세요.</option>
				<form:options items="${deptInfoOneDepthCategory}" />
			</form:select>
			<form:select path="departmentid">
				<option value="">근무부서를 선택하세요.</option>
				<form:options items="${deptInfoTwoDepthCategory}" />
			</form:select>
		</td>
	</tr>
...
</form:form>
 
<ajax:select 
    baseUrl="${pageContext.request.contextPath}/autoSelectDept.do"  
    parameters="depth=2,superdeptid={superdeptid}" 
    source="superdeptid" 
    target="departmentid" 
    emptyOptionName="Select model"/>
...

ajax:tabPanel, ajax:tab

부서정보 페이지에서 각 상위부서에 속한 하위부서리스트를 보여줄때, tab으로 처리해서 보여준다.

ajax-tabpanel-sample

JSP

/easycompany/webapp/WEB-INF/jsp/departmentlist.jsp

...
<%@ taglib prefix="ajax" uri="http://ajaxtags.sourceforge.net/tags/ajaxtags" %>
...
<!--Ajax Tags-->
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/prototype.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/scriptaculous/scriptaculous.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/overlibmws/overlibmws.js"></script>
<script type="text/javascript" src="<%=request.getContextPath()%>/ajaxtags/js/ajaxtags.js"></script>
<link type="text/css" rel="stylesheet" href="<%=request.getContextPath()%>/ajaxtags/css/ajaxtags.css" />
<link type="text/css" rel="stylesheet" href="<%=request.getContextPath()%>/ajaxtags/css/displaytag.css" />
...
		<ajax:tabPanel id="departmentTab">
		<c:forEach items="${departmentlist}" var="departmentinfo" varStatus="status">
			<c:choose>
				<c:when test="${status.first}">
			        <ajax:tab caption="${departmentinfo.deptname}" 
					  baseUrl="/easycompany/subDepartmentList.do?superdeptid=${departmentinfo.deptid}&depth=2"
					  defaultTab="true"/>
				</c:when>
				<c:otherwise>
				<ajax:tab caption="${departmentinfo.deptname}" 
					  baseUrl="/easycompany/subDepartmentList.do?superdeptid=${departmentinfo.deptid}&depth=2"/>
				</c:otherwise>
			</c:choose>
		</c:forEach>
		</ajax:tabPanel>
...

참고자료

3.19 - Internationalization(국제화)

Spring MVC는 Cookie, Session, AcceptHeader를 이용한 LocaleResolver를 제공하며, 다국어 메시지는 ResourceBundleMessageSource를 통해 관리된다. 사용자는 LocaleChangeInterceptor를 통해 언어를 변경하고, JSP에서 <spring:message> 태그로 해당 언어에 맞는 메시지를 표시할 수 있다.

Internationalization(국제화)

개요

전자정부 표준 프레임워크에서는 Spring MVC 에서 제공하는 LocaleResolver를 이용한다. 우리는 여기서 LocaleResolver를 알아보고 적용하는 설정과 다국어가 적용된 message resource 를 가져와 활용하는 것을 보도록 하겠다. Spring MVC 는 다국어를 지원하기 위하여 아래와 같은 종류의 LocaleResolver 를 제공하고 있다.

  • CookieLocaleResolver
    • 쿠키를 이용한 locale정보 사용
  • SessionLocaleResolver
    • 세션을 이용한 locale정보 사용
  • AcceptHeaderLocaleResolver
    • 클라이언트의 브라우져에 설정된 locale정보 사용

Bean 설정 파일에 정의하지 않을 경우 AcceptHeaderLocaleResolver 를 default 로 적용된다.

설명

3가지의 LocaleResolver

CookieLocaleResolver

CookieLocaleResolver 를 설정하는 경우 사용자의 쿠키에 설정된 Locale 을 읽어 들인다. samlple-servlet.xml

...
<bean id="localeResolver"
    class="org.springframework.web.servlet.i18n.CookieLocaleResolver" >
    <property name="cookieName" value="clientlanguage"/>   
    <property name="cookieMaxAge" value="100000"/>
    <property name="cookiePath" value="web/cookie"/>
</bean>
...

다음과 같은 속성을 사용할 수 있다.

속성기본값설명
cookieNameclassname + locale쿠키 명
cookieMaxAgeinteger.MAX_INT-1 로 해두면 브라우저를 닫을 때 없어짐
cookiepath/Path 를 지정하면 해당하는 Path와 그 하위 Path 에서만 참조

SessionLocaleResolver

requst가 가지고 있는 session으로 부터 locale 정보를 가져온다. samlple-servlet.xml

...
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver" />
...

AcceptHeaderLocaleResolver

사용자의 브라우저에서 보내진 request 의 헤더에 accept-language 부분에서 Locale 을 읽어 들인다. 사용자의 브라우저의 Locale 을 나타낸다. samlple-servlet.xml

<bean id="localeResolver" class="org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver" />

XML 설정

Web Configuration

Web 을 통해 들어오는 요청을 Charset UTF-8 적용한다.

CharacterEncodingFilter 을 이용하여 encoding 할 수 이도록 아래와 같이 세팅한다.

web.xml

...
   <filter>
       <filter-name>encoding-filter</filter-name>
       <filter-class>
           org.springframework.web.filter.CharacterEncodingFilter
       </filter-class>
       <init-param>
           <param-name>encoding</param-name>
           <param-value>UTF-8</param-value>
       </init-param>
   </filter>
 
  <filter-mapping>
      <filter-name>encoding-filter</filter-name>
      <url-pattern>/*</url-pattern>
  </filter-mapping>
...

Spring Configuration

사용자의 브라우저의 Locale 정보를 이용하지 않고 사용자가 선택하여 언어를 직접 선택할 수 있도록 구현하려 한다면 CookieLocaleResolver 나 SessionLocaleResolver 를 이용한다. 먼저 다국어를 지원해야 하므로 메세지를 MessageSource 로 추출하여 구현해야 한다. messageSource는 아래와 같이 설정하였다.

samlple-servlet.xml

context-common.xml 등에 설정된 경우 설정할 필요는 없다.

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basenames">
    <list>
        <value>classpath:/message/message</value>
    </list>
    </property>
</bean>

XML 설정

message properties 파일은 아래와 같다. locale에 따라 ko, en 으로 구분하였다. message_ko.properties

view.category=카테고리

message_en.properties

view.category=category

ResourceBundleMessageSource 는 beannames 명으로 messages 을 받아오는데 디폴트의 경우는 messages.properties 에서 message를 받아오고 locale 이 한국어일 경우는 messages_ko_KRproperties 에서 받아오고 영어일 경우는 messages_en_US.properties 에서 받아온다. 아래와 같이 localeResover 과 localeChangeInterceptor 를 등록하고 Annotation 기반에서 동작할 수 있도록 DefaultAnnotationHandlerMapping 에 interceptor 로 등록을 해준다.

samlple-servlet.xml (Spring 3.0이하)

<!-- 세션을 이용한 Locale 이용시-->
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"/>
 
<!-- 쿠키를 이용한 Locale 이용시  
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
-->
<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
	<property name="paramName" value="language"/>
</bean>
 
<bean id="annotationMapper" class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
	<property name="interceptors">
		<list>
			<ref bean="localeChangeInterceptor"/>
		</list>
	</property>
</bean>

samlple-servlet.xml (Spring 3.1이상, 실행환경 대부분 버전이 해당된다.)

<!-- 세션을 이용한 Locale 이용시-->
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"/>
 
<!-- 쿠키를 이용한 Locale 이용시  
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
-->
<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
	<property name="paramName" value="language"/>
</bean>
 
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    <property name="webBindingInitializer">
        <bean class="egovframework.com.cmm.web.EgovBindingInitializer"/>
    </property>
</bean>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
    <property name="interceptors">
        <list>
            <ref bean="localeChangeInterceptor" />
        </list>
    </property>
</bean>

설정 주의사항

<mvc:annotation-driven/>

위의 내용이 설정되어 있을 경우 상단의 RequestMappingHandlerAdapter, RequestMappingHandlerMapping 설정이 적용되지 않는다. mvc:annotation-driven이 기본 설정으로 위 bean을 생성하므로 servlet xml에 선언되어 있을 경우 주석 처리한다. 세부 내용은 Spring MVC Tag Configuration 을 참고한다. mvc:annotation-driven을 사용하려면 인터셉터를 명시적으로 선언해야 한다. RequestMappingHandlerAdapter, RequestMappingHandlerMapping Bean의 설정 내용을 삭제하고, 아래의 코드를 추가한다.

<mvc:interceptors>
	<mvc:interceptor>
		<mvc:mapping path="/**" />
		<bean id="localeChangeInterceptor" class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
	        <property name="paramName" value="language" />
	    </bean>
	</mvc:interceptor>
</mvc:interceptors>

SessionLocaleResolver 를 이용하여 위와 같이 하였을 경우 Locale 결정은 language 로 Request Parameter 로 넘기게 된다. 카테고리 용어가 영어와 한글로 바뀌는 것을 아래와 같이 볼 수 있다.

리스트를 보여주는 화면에 예를 보자면 Spring 메시지 태그를 이용하여

<spring:message code="view.category" />

으로 표현한다.

<%@ taglib prefix="spring" uri=http://www.springframework.org/tags %>
 
<form:form commandName="message" >
....
		<table border="1" cellspacing="0" class="boardList" summary="List of Category">
			<thead>
				<tr>
					<th scope="col">No.</th>
					<th scope="col">
						<input name="checkAll" type="checkbox" class="inputCheck" title="Check All" onclick="javascript:fncCheckAll();"/>
					</th>
					<th scope="col"><spring:message code="view.category" /> ID</th>
					<th scope="col"><spring:message code="view.category" /> 명</th>
					<th scope="col">사용여부</th>
					<th scope="col">Description</th>
					<th scope="col">등록자</th>
				</tr>
			</thead>
....

화면상으로 해당 페이지를 실행해보면 아래와 같다.

한글인 경우 :

http://localhost:8080/sample-web/sale/listCategory.do?language=ko

internationalization-ko

영어인 경우 :

http://localhost:8080/sample-web/sale/listCategory.do?language=en

internationalization-en

Java 소스내에서 locale 적용 메시지 가져오기

참고로 MessageSource 는 아래와 같은 메소드로 이루어져 있다.(실제로 여기서의 구현체는 ResourceBundleMessageSource 임.)

internationalization-locale

String msg = messageSource.getMessage(messageKey, messageParameters,	defaultMessage, locale);

참고자료

3.20 - Security Service

입력값 유효성 검증을 위한 기능으로 Jakarta Commons Validator를 선택하여 Spring MVC와 연계해 활용하는 방법을 설명한다. 이를 통해 화면 처리 레이어에서 입력값 검증 기능을 제공하고, Spring Security를 활용한 공통 기반 레이어에서 인증 및 권한과 같은 보안 서비스를 지원한다.

Security Service

개요

인증, 권한 같은 일반적인(통상적인) 개념의 Security 서비스는 Spring Security를 활용한 공통기반 레이어에서 제공한다. 화면 처리 레이어의 Security 서비스는 입력값 유효성 검증 기능을 제공한다. 입력값 유효성 검증(validation)을 위한 기능은 Valang, Jakarta Commons, Spring 등에서 제공하는데, 여기서는 기반 오픈소스로 Jakarta Commons Validator를 선택했다. MVC 프레임워크인 Spring MVC와 Jakarta Commons Validator의 연계와 활용방안을 설명한다.

설명

Jakarta Commons Validator는 필수값, 각종 primitive type(int,long,float…), 최대-최소길이, 이메일, 신용카드번호등의 값 체크등을 할 수 있도록 Template이 제공된다. 또한 client-side, server-side의 검증을 함께 할 수 있으며, Configuration과 에러메시지를 client-side, server-side 별로 따로 하지 않고 한곳에 같이 쓰는 관리상의 장점이 있다. 자세한 설명은 아래의 문서를 참조하라.

참고자료

3.21 - Spring Framework + Jakarta Commons Validator

Jakarta Commons Validator는 필수값, 길이, 이메일 등의 검증을 제공하며, Java와 JavaScript로 구현되어 클라이언트와 서버 양쪽에서 검증을 수행할 수 있다. Spring Framework에서는 Spring Modules를 통해 Commons Validator와 연동이 가능하며, 이를 통해 서버와 클라이언트 검증 설정을 통합적으로 관리할 수 있다. 본 문서는 설치 방법과 핵심 클래스(DefaultValidatorFactory, DefaultValidator), 설정 파일, 예제 프로젝트 적용 과정을 설명한다.

Spring Framework + Jakarta Commons Validator

개요

입력값 검증을 위한 Validation 기능은 Valang, Jakarta Commons, Spring 등에서 제공한다.
여기서는 Jakarta Commons Validator를 Spring Framework과 연동하여 사용하는 방법에 대해서 설명하고자 한다.
Jakarta Commons Validator는 필수값, 각종 primitive type(int,long,float…), 최대-최소길이, 이메일, 신용카드번호등의 값 체크등을 할 수 있도록 Template이 제공된다.
이 Template은 Java 뿐 아니라 Javascript로도 제공되어 client-side, server-side의 검증을 함께 할 수 있으며, Configuration과 에러메시지를 client-side, server-side 별로 따로 하지 않고 한곳에 같이 쓰는 관리상의 장점이 있다.
Struts에서는 Commons Validator를 사용하기 위한 org.apache.struts.validator.ValidatorPlugIn 같은 플러그인 클래스를 제공하는데, Spring에서는 Spring Modules 프로젝트에서 연계 모듈을 제공한다.
여기서는 server-side, client-side validation을 위해, 설치방법, Spring Module에서 제공하는 핵심 클래스인 DefaultValidatorFactory, DefaultValidator와 설정파일인 validator-rules.xml, validator.xml 에 대한 간략한 설명과 예제 프로젝트인 easycompany에 적용하는 과정을 설명한다.

설명

필요라이브러리

  1. commons-validator : Ver 1.4.0 아래 4개의 파일에 대한 dependency가 있다.
    • commons-beanutils : Ver 1.8.3
    • commons-digester : Ver 1.8
    • commons-logging : Ver 1.1.2
    • junit : Ver 3.8.2
  2. spring modules : Ver 0.9 다운 받고 압축을 풀어 보면 여러 파일들이 있으나 여기서 필요한 건 spring-modules-validation.jar 뿐이다. 예제를 보려면 \samples\sources\spring-modules-validation-commons-samples-src.zip도 필요하다.
  3. 위에서 언급한 라이브러리들을 설치한다.

DefaultValidatorFactory, DefaultBeanValidator 설정

Spring Validator와 Commons Validator의 연계를 위해 중요한 역할을 하는 클래스인 DefaultValidatorFactory, DefaultBeanValidator에 대해 간략하게 설명하자면 아래 표와 같다.

DefaultValidatorFactoryDefaultBeanValidator
프로퍼티 ‘validationConfigLocationsApache’에 정의된 Validation rule을 기반으로 Commons Validator들의 인스턴스를 얻는다.DefaultBeanValidator는 org.springframework.validation.Validator를 implements하고 있지만, DefaultValidatorFactory가 가져온 Commons Validator의 인스턴스를 이용해 validation을 수행한다. Controller에 validation 수행할때 이 DefaultBeanValidator를 참조하면 된다.

아래 코드를 참조해 빈 정의 파일(예제에는 easycompany-servlet.xml)에 다음과 같이 ValidatorFactory,Validator,validator-rules.xml,validation.xml 파일을 등록한다.

...
<!-- Integration Apache Commons Validator by Spring Modules -->				
    <bean id="beanValidator" class="org.springmodules.validation.commons.DefaultBeanValidator">
	<property name="validatorFactory" ref="validatorFactory"/>
    </bean>
 
    <bean id="validatorFactory" class="org.springmodules.validation.commons.DefaultValidatorFactory">
	<property name="validationConfigLocations">
		<list>
                      <!-- validator-rules.xml, validator.xml의 위치-->
			<value>/WEB-INF/conf/validator-rules.xml</value>
			<value>/WEB-INF/conf/validator.xml</value>
		</list>
	</property>
    </bean>
...

validator-rules.xml 설정

validator-rules.xml은 application에서 사용하는 모든 validation rule에 대해 정의하는 파일이다. 예제에 있는 validator-rules.xml의 필수값 체크 부분을 보면 아래와 같이 작성되어 있다.

      <validator name="required"
            classname="org.springmodules.validation.commons.FieldChecks"
               method="validateRequired"
         methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"
                  msg="errors.required">
         <javascript><![CDATA[
         .....
            ]]>
         </javascript>
      </validator>
namevalidation rule(required,mask,integer,email…)
classnamevalidation check를 수행하는 클래스명(org.springmodules.validation.commons.FieldChecks)
methodvalidation check를 수행하는 클래스의 메소드명(validateRequired,validateMask…)
methodParamsvalidation check를 수행하는 클래스의 메소드의 파라미터
msg에러 메시지 key
javascriptclient-side validation을 위한 자바스크립트 코드

Spring Modules에서 제공하는 validation rule에 따라 구성해보자.

Spring Modules (0.9 기준)에서 제공하는 validation rule들은 아래와 같다.

name(validation rule)FieldCheck 클래스FieldCheck 클래스의 메소드기능
requiredorg.springmodules.validation.commons.FieldChecksvalidateRequired필수값 체크
minlengthorg.springmodules.validation.commons.FieldChecksvalidateMinLength최소 길이 체크
maxlengthorg.springmodules.validation.commons.FieldChecksvalidateMaxLength최대 길이 체크
maskorg.springmodules.validation.commons.FieldChecksvalidateMask정규식 체크
byteorg.springmodules.validation.commons.FieldChecksvalidateByteByte형 체크
shortorg.springmodules.validation.commons.FieldChecksvalidateShortShort형 체크
integerorg.springmodules.validation.commons.FieldChecksvalidateIntegerInteger형 체크
longorg.springmodules.validation.commons.FieldChecksvalidateLongLong형 체크
floatorg.springmodules.validation.commons.FieldChecksvalidateFloatFloat형 체크
doubleorg.springmodules.validation.commons.FieldChecksvalidateDoubleDouble형 체크
dateorg.springmodules.validation.commons.FieldChecksvalidateDateDate형 체크
rangeorg.springmodules.validation.commons.FieldChecksvalidateIntRange범위 체크
intRangeorg.springmodules.validation.commons.FieldChecksvalidateIntRangeint형 범위 체크
floatRangeorg.springmodules.validation.commons.FieldChecksvalidateFloatRangeFloat형 범위체크
creditCardorg.springmodules.validation.commons.FieldChecksvalidateCreditCard신용카드번호체크
emailorg.springmodules.validation.commons.FieldChecksvalidateEmail이메일체크

validator-rules.xml을 직접 작성하는것 보다는 예제에 있는 파일을 참고하거나 copy해서 사용하면 편리하다.
spring-modules-0.9.zip을 압축을 풀어보면 예제코드가 있다.(\samples\sources\spring-modules-validation-commons-samples-src.zip) 예제코드의 validator-rules.xml(\webapp\WEB-INF\validator-rules.xml)에는 모든 validation rule이 이미 정의되어 있다.

org.springmodules.validation.commons.FieldChecks 클래스에는 필수값 체크, primitive 타입 체크등 여러 validation을 수행하는 메소드들이 있다.
FieldChecks 클래스 소스를 열어 보면 실제 validation 처리는 Commons Validator에 위임하고 있다.
따라서 주의 할점은 Commons Validator에서 제공하는 validation rule중에, FieldChecks 클래스가 제공하지 않는 rule도 있다는 것이다.
예를 들어 Commons Validator 1.3.1 에서는 URL이나 IP 관련 Validator를 제공하지만, Spring Modules의 FieldChecks 클래스에 해당 메소드가 없기 때문에, 새로운 FieldCheck 클래스를 추가한 후 validation-rules.xml에 클래스와 메소드를 등록해야 사용할 수 있다.
Spring Modules가 제공하는 validation rule외에 rule을 추가하는 방법에 대해서는 주민등록번호 validation rule 추가하기 를 참고하라.

validator.xml 설정

validator.xml은 validation rule과 validation할 Form을 매핑한다.
form name과 field property의 name-rule은 Server-side와 Client-side인 경우에 따라 다르다.

Server-side validation의 경우는,
form name과 field property는 validation할 폼 클래스의 이름, 필드과 각각 매핑된다.(camel case) 폼 클래스가 Employee면 employee, DepartmentForm 이면 departmentForm을 form name으로 지정하라.

Client-side의 경우는,
form name은 JSP에서 설정한 <validator:javascript formName=“employee” …/> 태그의 formName와 매핑되고, field property는 각각의 폼 필드의 이름과 일치하면 된다.

따라서, Server-side, Client-side 둘 다 수행하려면,
JSP의 <validator:javascript formName=“employee” …/> 태그의 formName은 폼 클래스의 이름이 되어야 하고, JSP의 폼 필드들은 폼 클래스의 필드와 일치해야 한다.

depends는 해당 필드에 적용할 (validator-rules.xml에 정의된 rule name) validator를 의미한다.
<arg key…>는 메시지 출력시 파라미터를 지정하는 값이다.
아래와 같이 작성했다면, Employee 클래스의 name 필드에 대해서 필수값 체크를, age 필드에 대해서 integer 체크를, email 필드에 대해선 필수값과 email 유효값 체크를 하겠다는 의미이다

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE form-validation PUBLIC 
    "-//Apache Software Foundation//DTD Commons Validator Rules Configuration 1.1//EN" 
    "http://jakarta.apache.org/commons/dtds/validator_1_1.dtd">
 
<form-validation>
 
    <formset>
 
        <form name="employee">
        	<field property="name" depends="required">
        		<arg0 key="employee.name" />
		</field>
		<field property="age" depends="integer">
        		<arg0 key="employee.age" />
		</field>
		<field property="email" depends="required,email">
        		<arg0 key="employee.email" />
		</field>		
        </form>
 
    </formset>
 
</form-validation>

Server-Side Validation

Controller

Server-Side Validation을 위해 Controller에 validation 로직을 추가해 보자.
위 설정 파일에서 등록한 DefaultBeanValidator가 Controller에서 validation을 수행한다.
DefaultBeanValidator는 Commons Validator의 기능을 사용하지만 자신은 Spring Validator이기 때문에, 사용자는 Spring Validator를 사용하듯이 Controller에서 validation 코딩을 하면 된다.

package com.easycompany.controller.annotation;
...
import org.springmodules.validation.commons.DefaultBeanValidator;
...
 
@Controller
public class UpdateEmployeeController {	
	...
	@Autowired
	private DefaultBeanValidator beanValidator;
        ...
	@RequestMapping(value = "/updateEmployee.do", method = RequestMethod.POST)
	public String updateEmployee(@ModelAttribute("employee") Employee employee,			
			BindingResult bindingResult, Model model) {
 
		beanValidator.validate(employee, bindingResult); //validation 수행
		if (bindingResult.hasErrors()) { //만일 validation 에러가 있으면...
			.....
			return "modifyemployee";
		}
		employeeManageService.updateEmployee(employee);
		return "changenotify";
	}        
}

JSP

Validation을 적용할 JSP를 작성한다.(modifyemployee.jsp)
form submit을 하면 이름, 나이, 이메일등의 입력값이 Employee 클래스에 바인딩이 되서 Controller에 전달이 되고, Controller에 validation 수행 로직이 있으면 validator.xml 내용에 따라 validation이 이루어 진다.
만일 에러가 발생하면 <form:error…/>에 에러에 해당하는 메시지를 출력한다. 에러 메시지에 관련해서는 아래 에러 메시지 등록을 참고하라.

<form:form commandName="employee">
<table>
        ....
	<tr>
		<th>이름</th>
		<td><form:input path="name" size="20"/><form:errors path="name" /></td>
	</tr>
	<tr>
		<th>비밀번호</th>
		<td><form:input path="password" size="10"/></td>
	</tr>
	<tr>
		<th>나이</th>
		<td><form:input path="age" size="10"/><form:errors path="age" /></td>
	</tr>
	<tr>
		<th>이메일</th>
		<td><form:input path="email" size="50"/><form:errors path="email" /></td>
	</tr>
</table>
<table width="80%" border="1">
	<tr>
		<td>
		<input type="submit"/>
		<input type="button" value="LIST" onclick="location.href='/easycompany/employeeList.do'"/>
		</td>
	</tr>
</table>
 
</form:form>

에러 메시지 등록

메시지 파일에 에러 메시지를 등록한다.
validation 에러가 발생하면 validator-rules.xml에서 정의했던 msg값으로 에러메시지의 key값을 찾아 해당하는 메시지를 출력해준다.
예를 들어, email validation에서 에러가 발생하면 msg값이 errors.email 이므로, “유효하지 않은 이메일 주소입니다.”라는 에러 메시지를 JSP에 있는 <form:errors path=“email” /> 부분에 출력하게 된다.

employee.name=이름
employee.email=이메일
employee.age=나이
employee.password=비밀번호

# -- validator errors --
errors.required={0}은 필수 입력값입니다.
errors.minlength={0}은 {1}자 이상 입력해야 합니다.
errors.maxlength={0}은 {1}자 이상 입력할수 없습니다.
errors.invalid={0}은 유효하지 않은 값입니다.

errors.byte={0}은  byte타입이어야 합니다.
errors.short={0}은  short타입이어야 합니다.
errors.integer={0}은 integer 타입이어야 합니다.

....

errors.email=유효하지 않은 이메일 주소입니다.

TEST

이름 필드에 값을 비우고 submit하면, name에 필수값(required) validation rule이 설정되어 있으므로 아래와 같이 이름 필드 옆에 에러 메시지가 출력 될 것이다.

server-validate

Client-Side Validation

validator.jsp 추가

아래와 같은 내용의 validator.jsp를 작성한다.

<%@ page language="java" contentType="javascript/x-javascript" %>
<%@ taglib prefix="validator" uri="http://www.springmodules.org/tags/commons-validator" %>
<validator:javascript dynamicJavascript="false" staticJavascript="true"/>

/validator.do로 호출하도록 Controller에서 메소드를 추가하고 requestmapping 한다.
validator.jsp를 http://localhost:8080/easycompany/validator.do 브라우져에서 호출해보면, validator-rules.xml에서 정의한 javascript 함수들이 다운로드 되거나 화면에 print 되는 걸 확인할 수 있을 것이다.
validator.jsp는 client-validation을 위해 validator-rules.xml에서 정의한 javascript 함수들을 로딩한다.
따라서 client-validation을 할 페이지에서는 이 validator.jsp을

<script type="text/javascript" src="<c:url value="/validator.do"/>"></script>

같이 호출한다.

JSP 설정(taglib,javascript) 추가

client-validation을 위해서는 해당 JSP에 아래와 같은 작업이 추가 되어야 한다. commons-validator taglib를 선언한다.

<%@ taglib prefix="validator" uri="http://www.springmodules.org/tags/commons-validator" %>

필요한 자바 스크립트 함수를 generate 하기 위한 코드를 추가 한다. validation-rules.xml에서 선언한 함수를 불러 오기 위해, 위에서 작성한 validator.jsp를 아래와 같이 호출한다.

<script type="text/javascript" src="<c:url value="/validator.do"/>"></script>

위의 자바 스크립트 함수를 이용해 필요한 validation과 메시지 처리를 위한 자바 스크립트를 generate 하기 위한 코드를 추가 한다. formName에는 validator.xml에서 정의한 form의 이름을 써준다.

<validator:javascript formName="employee" staticJavascript="false" xhtml="true" cdata="false"/>

form submit시에 validateVO클래스명() 함수를 호출한다.

   ... onsubmit="return validateEmployee(this)" ">

따라서 앞의 server-side validation에서 작성한 modifyemployee.jsp은 아래와 같이 변경된다.

<!-- commons-validator tag lib 선언-->
<%@ taglib prefix="validator" uri="http://www.springmodules.org/tags/commons-validator" %>
 
....
<!--for including generated Javascript Code(in validation-rules.xml)-->
<script type="text/javascript" src="<c:url value="/validator.do"/>"></script>
<!--for including generated Javascript Code(validateEmployee(), formName:validator.xml에서 정의한 form의 이름)-->
<validator:javascript formName="employee" staticJavascript="false" xhtml="true" cdata="false"/>
<script type="text/javascript">
	function save(form){	
		if(!validateEmployee(form)){
			return;
		}else{
			form.submit();
		}
	}
</script>
....
<form:form commandName="employee">
<table>
        ....
	<tr>
		<th>이름</th>
		<td><form:input path="name" size="20"/><form:errors path="name" /></td>
	</tr>
	<tr>
		<th>비밀번호</th>
		<td><form:input path="password" size="10"/></td>
	</tr>
	<tr>
		<th>나이</th>
		<td><form:input path="age" size="10"/><form:errors path="age" /></td>
	</tr>
	<tr>
		<th>이메일</th>
		<td><form:input path="email" size="50"/><form:errors path="email" /></td>
	</tr>
</table>
<table width="80%" border="1">
	<tr>
		<td>
		<!--<input type="submit"/>-->
                <input type="button" value="SAVE" onclick="save(this.form)"/> <!-- client-validation을 위해 바로 submit하지 않고 먼저 validateEmployee 함수를 호출-->
		<input type="button" value="LIST" onclick="location.href='/easycompany/employeeList.do'"/>
		</td>
	</tr>
</table> 
</form:form>
....

TEST

이번에도 이름 필드의 값을 지우고 저장 버튼을 누르면 아래와 같은 alert 메시지가 보일 것이다.

client-validate

참고문헌

  • Spring Modules Reference Documentation v 0.9, Chapter 17.Validation, P133~136

3.22 - Commons Validator에 validation rule 추가하기

Commons Validator는 기본적인 유형 외에도 프로젝트 특성에 맞는 공통 validation rule을 추가할 수 있다. 예제로 주민등록번호 검증기를 추가하여 validation rule을 확장하는 방법을 설명하며, 이를 easycompany 프로젝트에 적용했다.

Commons Validator에 validation rule 추가하기

개요

Commons Validator는 primitive type, 필수값, 이메일등 흔히 사용되는 유형에 대한 validation rule을 template으로 제공하지만, 프로젝트의 특성에 따라 공통으로 사용되는 validation rule이 발생되고 이를 추가해야할 필요가 생길 수 있다. 공공프로젝트에서 흔히 사용되는 주민등록번호 validator를 추가해 봄으로써, validation rule을 추가하는 방법을 알아보고자 한다.
예제는 easycompany를 이용했다.

설명

Spring Module을 이용해서 Commons Validator를 사용한다면 아래와 같은 내용을 validation rule 정의 파일(validator-rules.xml 같은)에서 보았을 것이다.

    <!--필수값 체크 validation rule-->
    <validator name="required"
          classname="org.springmodules.validation.commons.FieldChecks"
             method="validateRequired"
       methodParams="java.lang.Object,
                     org.apache.commons.validator.ValidatorAction,
                     org.apache.commons.validator.Field,
                     org.springframework.validation.Errors"
                msg="errors.required">
       <javascript><![CDATA[
          ...
          ]]>
       </javascript>
    </validator>

validator 태그의 각각의 attribute는 다음과 같은 의미를 같는다.

namevalidation rule(required,mask,integer,email…)
classnamevalidation check를 수행하는 클래스명
methodvalidation check를 수행하는 클래스의 메소드명
methodParamsvalidation check를 수행하는 클래스의 메소드의 파라미터
msg에러 메시지 key
javascriptclient-side validation을 위한 자바스크립트 코드

주민등록번호 rule을 아래와 같이 추가한다고 하면,

    <validator name="ihidnum"
          classname="egovframework.rte.ptl.mvc.validation.RteFieldChecks"
             method="validateIhIdNum"
       methodParams="java.lang.Object,
                     org.apache.commons.validator.ValidatorAction,
                     org.apache.commons.validator.Field,
                     org.springframework.validation.Errors"                       
            depends=""
                msg="errors.ihidnum">
         <javascript><![CDATA[
          ...
          ]]>
       </javascript>
    </validator>

필요한 작업은 아래와 같다.

  1. FieldCheck class(RteFieldChecks) 작성
  2. Validator class(RteGenericValidator) 작성
  3. validator-rules.xml 설정
  4. validator.xml 설정
  5. 에러메시지 설정
  6. TEST

FieldCheck class 작성

org.springmodules.validation.commons.FieldChecks를 상속 받는 RteFieldChecks 클래스를 생성한다. 그리고 주민등록번호 validation을 담당할 validateIhIdNum 메소드를 추가한다. 주민등록번호 validation 로직이 RteFieldChecks.validateIhIdNum() 안에 있어도 되지만, org.springmodules.validation.commons.FieldChecks와 같은 방식으로 다른 Validator에 위임했다.

package egovframework.rte.ptl.mvc.validation;
 
import org.apache.commons.validator.Field;
import org.apache.commons.validator.ValidatorAction;
import org.springframework.validation.Errors;
import org.springmodules.validation.commons.FieldChecks;
 
public class RteFieldChecks extends FieldChecks{
 
	public static boolean validateIhIdNum(Object bean, ValidatorAction va,
            Field field, Errors errors){
		//bean에서 해당 field 값을 추출
		String ihidnum = FieldChecks.extractValue(bean, field); 
 
                //주민등록번호 유효성 검사 알고리즘은 RteGenericValidator가 가지고 있다.
		if(!RteGenericValidator.isValidIdIhNum(ihidnum)){ //유효한 주민등록번호가 아니면
			FieldChecks.rejectValue(errors, field, va); //에러 처리
			return false;
		}else{
			return true;
		}
	}
}

Validator class 작성

주민등록번호 유효성체크 로직이 있는 RteGenericValidator 클래스를 작성해 보자. 유효성 체크 기준은

  1. 값의 길이가 13자리이며, 7번째 자리가 1,2,3,4 중에 하나인가?
  2. 앞 6자리의 값이 유효한 날짜인가?
  3. 주민등록번호 마지막 자리를 이용한 check

로 했다.

package egovframework.rte.ptl.mvc.validation;
import java.io.Serializable;
import org.apache.commons.validator.GenericTypeValidator;
 
public class RteGenericValidator implements Serializable {	
	public static boolean isValidIdIhNum(String value) {		
		//값의 길이가 13자리이며, 7번째 자리가 1,2,3,4 중에 하나인지 check.
		String regex = "\\d{6}[1234]\\d{6}";
		if (!value.matches(regex)) {
			return false;
		}
 
		//앞 6자리의 값이 유효한 날짜인지 check.
		try {
			String strDate = value.substring(0, 6);
			strDate = ((value.charAt(6) == '1' || value.charAt(6) == '2') ? "19":"20") + strDate;
			strDate = strDate.substring(0, 4) + "/" + strDate.substring(4, 6)
					+ "/" + strDate.substring(6, 8);
 
			SimpleDateFormat dateformat = new SimpleDateFormat("yyyy/MM/dd");
			Date date = dateformat.parse(strDate);
			String resultStr = dateformat.format(date);
 
			if (!resultStr.equalsIgnoreCase(strDate)) {
				return false;
			}
 
		} catch (ParseException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return false;
		}
 
		//주민등록번호 마지막 자리를 이용한 check.
		char[] charArray = value.toCharArray();
		long sum = 0;
		int[] arrDivide = new int[] { 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5 };
		for (int i = 0; i < charArray.length - 1; i++) {
			sum += Integer.parseInt(String.valueOf(charArray[i]))
					* arrDivide[i];
		}
 
		int checkdigit = (int) ((int) (11 - sum % 11)) % 10;
		return (checkdigit == Integer.parseInt(String.valueOf(charArray[12]))) ? true
				: false;
	}
}

validator-rules.xml 설정

이제 주민등록번호 validation rule을 추가해 보자. rule 이름은 ihidnum으로 하고 위에서 작성한 코드를 바탕으로 설정하면, (이미 위 개요에 나온 대로) 아래와 같다. client-validation을 위해서 자바스크립트 코드도 추가했다.

      <validator name="ihidnum"
            classname="egovframework.rte.ptl.mvc.validation.RteFieldChecks"
               method="validateIhIdNum"
         methodParams="java.lang.Object,
                       org.apache.commons.validator.ValidatorAction,
                       org.apache.commons.validator.Field,
                       org.springframework.validation.Errors"                       
              depends=""
                  msg="errors.ihidnum">
           <javascript><![CDATA[
           function validateIhIdNum(form) {
                var bValid = true;
                var focusField = null;
                var i = 0;
                var fields = new Array();
                oIhidnum = new ihidnum();                
                for (x in oIhidnum) {
                	var field = form[oIhidnum[x][0]];
                	if (field.type == 'text' ||
                    	field.type == 'hidden' ||
                        field.type == 'textarea') {
                        if (trim(field.value).length==0 || !checkIhIdNum(field.value)) {
                            if (i == 0) {
                                focusField = field;
                            }
                            fields[i++] = oIhidnum[x][1];
                            bValid = false;
                        }
                    }
                }
                if (fields.length > 0) {
                    alert(fields.join('\n'));
                }
                return bValid;
            }
 
            /**
             * Reference: JS Guide
             * http://jsguide.net/ver2/articles/frame.php?artnum=002
             */             
            function checkIhIdNum(ihidnum){
 
            	fmt = /^\d{6}[1234]\d{6}$/;
            	if(!fmt.test(ihidnum)){
            		return false;
            	}
 
            	birthYear = (ihidnum.charAt(7) <= "2") ? "19" : "20";
				birthYear += ihidnum.substr(0, 2);
				birthMonth = ihidnum.substr(2, 2) - 1;
				birthDate = ihidnum.substr(4, 2);
				birth = new Date(birthYear, birthMonth, birthDate);
 
				if( birth.getYear() % 100 != ihidnum.substr(0, 2) ||
				    birth.getMonth() != birthMonth ||
				    birth.getDate() != birthDate) {
				    return false;
				}
 
            	var arrDivide = [2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5];            	
            	var checkdigit = 0;            	
            	for(var i=0;i<ihidnum.length-1;i++){
            		checkdigit += parseInt(ihidnum.charAt(i)) * parseInt(arrDivide[i]);
            	}
            	checkdigit = (11 - (checkdigit%11))%10;
            	if(checkdigit != ihidnum.charAt(12)){
            		return false;
            	}else{
            		return true;
            	}
            }
            ]]>
         </javascript>
      </validator>

validator.xml 설정

ihidnum이란 필드에 주민등록번호 validation 체크를 해보자. validator.xml에 필요한 내용을 추가하고.

<form-validation>
    <formset>
        <form name="employee">
        	...
		<field property="ihidnum" depends="ihidnum"/>
                ... 
        </form>
    </formset>
</form-validation>

에러메시지 설정

messages 프로퍼티에 에러메시지를 추가하자.

errors.ihidnum=유효하지 않은 주민등록번호입니다.

TEST

주민등록번호 server-side,client-side validatin을 위한 환경은 다 갖추어졌다. 이제 EmployeeController에 validation을 추가하고,(이미 추가되어 있다면 pass)

package com.easycompany.controller.annotation;
...
import org.springmodules.validation.commons.DefaultBeanValidator;
...
 
@Controller
public class EmployeeController {	
	...
	@Autowired
	private DefaultBeanValidator beanValidator;
        ...
	@RequestMapping(value = "/updateEmployee.do", method = RequestMethod.POST)
	public String updateEmployee(@ModelAttribute("employee") Employee employee,			
			BindingResult bindingResult, Model model) {
 
		beanValidator.validate(employee, bindingResult); //validation 수행
		if (bindingResult.hasErrors()) { //만일 validation 에러가 있으면...
			.....
			return "modifyemployee";
		}
		employeeManageService.updateEmployee(employee);
		return "changenotify";
	}        
}

VO Class(com.easycompany.model.Employee.java)에 주민등록번호 필드를 추가하고,

package com.easycompany.model;
public class Employee {
        ....
	private String ihidnum;
	private String ihidnum1;
	private String ihidnum2;
        ...
	public String getIhidnum() {
		return ihidnum1+ihidnum2;
	}
	public String getIhidnum1() {		
		return ihidnum1;
	}
	public void setIhidnum1(String ihidnum1) {
		this.ihidnum1 = ihidnum1;
	}
	public String getIhidnum2() {
		return ihidnum2;
	}
	public void setIhidnum2(String ihidnum2) {
		this.ihidnum2 = ihidnum2;
        }
}

JSP(/easycompany/webapp/jsp/modifyemployee.jsp)에 주민등록번호 입력 필드를 추가하자.

...
	<tr>
		<th>주민번호</th>
		<td>
			<form:input path="ihidnum1" size="10"/> - <form:input path="ihidnum2" size="10"/><form:errors path="ihidnum" />
			<form:hidden path="ihidnum"/>
		</td>
	</tr>
...

주민등록번호를 입력하지 않거나, 틀린번호를 입력시엔 아래와 같은 경고창이 뜬다.

ihidnum-validation

틀린 입력값으로 client를 통과하더라도 Controller에서 validation이 추가로 동작하므로, server-side에서 validation error가 일어날것이다.

참고자료

  • Spring Modules Reference Documentation v 0.9, Chapter 17.Validation, P133~13

3.23 - UI Adaptor Service

전자정부 표준프레임워크에서는 Spring MVC Annotation을 기반으로 요청 URI와 Controller 메소드를 매핑하며, 메소드 파라미터로 업무용 DTO 객체를 사용할 수 있도록 가이드한다. UI 솔루션과의 연동 방식은 프로젝트 특성에 맞게 설계해야 하며, 보통 Controller에서 데이터를 DTO 형태로 변환하여 업무 로직으로 넘기는 방식이 많이 사용된다.

UI Adaptor Service

개요

전자정부 표준프레임워크와 UI 솔루션(Rich Internet Application) 연동에 대해 살펴 본다. UI Adaptor를 적용하는 방식은 특정한 하나의 방법을 표준화하기 어렵다. 보통 Web Framework 과 UI 솔루션과의 연동을 하는 방법 중 가장 많이 사용하는 방식은 Controller 역할을 수행하는 Servlet 객체에서 업무 로직을 호출 전 데이터를 DTO 형태로 변화하여 업무 로직으로 넘기는 방식이다.

전자정부 표준프레임워크에서는 Spring MVC Annotation 기반으로 개발 시 요청되는 URI 와 Controller 클래스내의 메소드를 매핑하고 있다. 따라서 메소드의 파라미터로 넘어오는 객체가 request 객체가 아닌 업무용 DTO 클래스로 넘어올 수 있도록 가이드 하는 방식을 선택했다. (사실 @ModelAttribute 를 이용하는 것과 같다.) 하지만 프로젝트 별로 비기능 요구사항의 특성을 고려하여 적합한 구조를 정의하여 적용하는 것이 필요하다.

UI 솔루션 업체별 상세 가이드

설명

중점적으로 우리가 살펴볼 내용은 Controller 앞단에서 UI 솔루션으로부터 넘어온 데이터를 DTO 로 변환하는 과정이다. 데이타 변환을 위해 우리는 ArgumentResolver 를 이용한 방법을 살펴보도록 하겠다. UI 솔루션으로 넘어오는 데이터 객체는 request 객체에 포함되어 넘어온다. 우리가 필요한 것은 업무용 DTO 클래스이다. 업무용 DTO 클래스는 URI(@RequestMapping)와 매핑 된 Controller 메소드의 파라미터로 존재하게 된다. Controller 메소드의 파라미터에 설정된 클래스(여기서는 DTO)를 AnnotationMethodHandlerAdapter 에서 그에 해당하는 ArgumentResolvers(customArgumentResolvers포함) 를 호출해준다. 따라서 우리는 ArgumentResolver를 확장하여 CustomRiaArgumentResolver 개발하여 AnnotationMethodHandlerAdapter에 등록한다. CustomRiaArgumentResolver 에서 리턴되는 객체는 Contorller 단의 메소드의 파라미터로 이용된다.

  • 참고 : AnnotationMethodHandlerAdapter 는 URI 와 매핑되는 Contorller 의 메소드를 실행 시 파라미터로 존재하는 객체타입에대한 ArgumentResolver를 실행하여 가져온다.

그리고 Controller단의 실행 결과는 ViewResovler를 통해 RiaView 로 전송되며 RiaVeiw는 결과물인 DTO 를 UI 솔루션 데이터 타입으로 변환하여 response로 보내어 진다. 다시 핵심적인 내용을 정리하자면 다음과 같다.

  1. AnnotationMethodHandlerAdapter 의 CustomRiaArgumentResolver 등록 ⇒ CustomRiaArgumentResolver
  2. UIAdaptor 등록 ⇒ UIAdaptorImpl
  3. RiaView 등록 ⇒ RiaView

사용되는 DTO 를 아래 클래스로 생각하고 살펴보겠다.

UdDTO.java(샘플)

...
public class UdDTO implements Serializable {
 
    private Map variableList ;
    private Map dataSetList ;
    private Map Objects ;
 
    public void setVariableList(Map variableList) {
	this.variableList= variableList;
    }
 
    public void setDataSetList(Map dataSetList) {
	this.dataSetList= dataSetList;
    }
 
    public Map getVariableList() {
        return variableList;
    }
 
    public Map getDataSetList() {
        return dataSetList;
    }
 
    public void setObjects(Map objects) {
	Objects = objects;
    }
 
    public Map getObjects() {
	return Objects;
    }
}
...

UI솔루션데이타에서 DTO로 변환

다음은 UTO 를 가져와 UI 솔루션에 의해 들어오는 객체로부터 UTO 로 변환하는 부분을 설명한다. 변환을 담당하는 UIAdaptorImpl 객체는 AnnotationMethodHandlerAdapter 의 CustomRiaArgumentResolver 에 설정된다.

설정정보(CustomRiaArgumentResolver)

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="webBindingInitializer">
        <bean class="egovframework.rte.fdl.web.common.EgovBindingInitializer" />
    </property>
    <property name="customArgumentResolvers">
        <list>
            <bean class="egovframework.rte.fdl.sale.web.CustomRiaArgumentResolver">
                <property name="uiAdaptor">
                    <ref bean="riaAdaptor" />
                </property>
            </bean>
        </list>
    </property>
</bean>

WebArgumentResolver의 구현체인 CustomRiaArgumentResolver 는 uiAdaptor 에 세팅된 Adaptor 를 실행해준다.

CustomRiaArgumentResolver.java

public class CustomRiaArgumentResolver implements WebArgumentResolver {
 
	private UiAdaptor uiA;
 
	public void setUiAdaptor(UiAdaptor uiA) {
		this.uiA = uiA;
	}
 
	public Object resolveArgument(MethodParameter methodParameter, NativeWebRequest webRequest) throws Exception {
 
		Class<?> type = methodParameter.getParameterType();
		Object uiObject = null;
 
		if (uiA == null)
			return UNRESOLVED;
 
		//Controller의 실행되는 메소드의 파라미터타입 정보가 MethodParameter를 통해 넘어온다. 
		//설정한 UIAdaptro 구현체에 등록되어 있는 UTO 와 비교한다.
		if (type.equals(uiA.getModelName())) {
			HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
			// 여기서 데이타 만들어 넘긴다.
			uiObject = (UdDTO) uiA.convert(request);
			return uiObject;
		}
 
		return UNRESOLVED;
	}
...

UiAdaptor 의 구현체는 아래와 같다. 여기서는 Miplatform 의 예를 들어 코드를 작성하였다. UI 솔루션의 객체에서 DTO 로 데이터를 옮기는 역할은 converte4In 메소드에서 수행된다.

RiaAdaptorImpl.java(MiPlatform ⇒ UdDTO)

public class RiaAdaptorImpl implements UiAdaptor {
 
  protected Log log = LogFactory.getLog(this.getClass());
 
 
  //resolveArgument 메소드에서 호출하는 메소드 
	public Object convert(HttpServletRequest request) throws Exception {
 
		PlatformRequest platformRequest = null;
 
		try {
			platformRequest = new PlatformRequest(request, PlatformRequest.CHARSET_UTF8);
			platformRequest.receiveData();
		} catch (IOException ex) {
			ex.getStackTrace();
			// throw new IOException("PlatformRequest error");
		}
    //UI 솔루션 데이터에서 DTO 객체로 변환
		UdDTO dto = converte4In(platformRequest);
		return dto;
	}
 
	private UdDTO converte4In(PlatformRequest platformRequest) {
		UdDTO dto = new UdDTO();
		//... DTO 또는 VO 값 채우기 
		.....
		return dto;
	}
 
	public Class getModelName() {
		return UdDTO.class;
	}
}

Controller 메소드 구현

UdDTO 클래스는 CustomRiaArgumentResolver 에서 만들어져 Controller 의 메소드의 parameter 형태로 가져온다. 아래 예는 Controller 단의 메소드 이다.

XXCategoryController.java

...
	@RequestMapping("/sample/miplatform.do")
	public ModelAndView selectCategoryList4Mi(UdDTO miDto, Model model) throws Exception {
 
		ModelAndView mav = new ModelAndView("riaView");
 
		//조회조건이 있을 경우 사용될 Map
		Map<String, String> smp = new HashMap<String, String>();
		try {
 
		  //Biz Layer 를 호출 한다. 
			List resultList = categoryService.selectCategoryList(smp);
 
			//결과값을 모델에 저장 
			mav.addObject("MiDTO", resultList);
 
		} catch (Exception ex) {
			log.info(ex.getStackTrace(), ex);
		}
		return mav;
	}

BeanNameViewResolver 설정

모델 객체의 이름이 riaView 이다. 이것은 Bean Name을 직접 명시한 것으로 아래와 같은 설정(BeanNameViewResolver)이 필요하다.

<bean class="org.springframework.web.servlet.view.BeanNameViewResolver" p:order="0" />
 
<bean class="org.springframework.web.servlet.view.UrlBasedViewResolver"
		p:order="1" p:viewClass="org.springframework.web.servlet.view.JstlView"
		p:prefix="/WEB-INF/jsp/" p:suffix=".jsp" />
 
<bean id="riaView" class="egovframework.rte.fdl.sale.web.RiaView" />

RiaView 구현

RiaView 의 코드는 아래와 같다. DTO 를 업체에 맞춰 다시 가져 나가기 위해 convert 하는 모듈이다. 여기서는 Miplatform 객체로 변환한다. egovDs 라는 DataSet 으로 객체화 한후 stream 형태로 보내는 로직이다. 따라서 업체별 데이타 형태로 변환하여 보내도록 수정하면 된다.

RiaView.java(DTO ⇒ MiPlatform)

....
public class RiaView extends AbstractView {
 
	protected Log log = LogFactory.getLog(this.getClass());
 
	@SuppressWarnings("unchecked")
	@Override
	protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response)
	        throws Exception {
		VariableList miVariableList = new VariableList();
		DatasetList miDatasetList = new DatasetList();
		PlatformData platformData = new PlatformData(miVariableList, miDatasetList);
 
		List list = (List) model.get("MiDTO");
 
		Iterator<Map> iterator = list.iterator();
		Iterator<Map> dataIterator = list.iterator();
 
		Dataset dataset = new Dataset("egovDs");
 
		while (iterator.hasNext()) {
			// Header 세팅
			Map<String, Object> record = iterator.next();
			Iterator<String> si = record.keySet().iterator();
 
			while (si.hasNext()) {
				String key = si.next();
				dataset.addColumn(key, ColumnInfo.COLUMN_TYPE_STRING, (short) 255);
			}
		}
 
		while (dataIterator.hasNext()) {
			Map<String, Object> record = dataIterator.next();
			Iterator<String> si = record.keySet().iterator();
			// Header 세팅
			while (si.hasNext()) {
				String key = si.next();
				dataset.addColumn(key, ColumnInfo.COLUMN_TYPE_STRING, (short) 255);
			}
 
			// Value 세팅
			int row = dataset.appendRow();
			Iterator<String> si2 = record.keySet().iterator();
			while (si2.hasNext()) {
				String key = si2.next();
				String value = (String) record.get(key);
 
				System.out.println("key = " + key + " , value = " + value);
				dataset.setColumn(row, key, value);
			}
			miDatasetList.add(dataset);
		}
		try {
 
			new PlatformResponse(response, PlatformConstants.CHARSET_UTF8).sendData(platformData);
 
		} catch (IOException ex) {
			if (log.isErrorEnabled()) {
				log.error("Exception occurred while writing xml to MiPlatform Stream.", ex);
			}
 
			throw new Exception();
		}
 
	}
 
}

참고자료

3.24 - Asynchronous request processing

Servlet 3.0과 Spring MVC 3.2 이상에서는 비동기 요청 처리를 통해 요청 쓰레드가 반환된 후에도 내부 쓰레드에서 비동기 작업을 처리할 수 있다. 이를 위해 Callable, DeferredResult, WebAsyncTask 등을 사용해 시간이 오래 걸리는 작업을 비동기로 처리하고, 완료 후 응답을 보낼 수 있다.

Asynchronous request processing

개요

기존의 요청 처리는 하나의 요청에 대해 한 개의 쓰레드를 사용하였다. 하나의 쓰레드에서 요청-응답 과정을 모두 처리하기 때문에 요청처리 이후 응답이 오기까지 쓰레드를 대기상태로 유지하였다. 그러나 서버와의 연결을 유지한채 대기상태로 있는 것이 아니라 서버와의 처리를 계속 이어가게 해주기 위해서는 이러한 기존의 처리에 한계가 있었다.

Servlet 3.0에서 제공하는 비동기 요청 처리는 쓰레드가 대기상태로 있는 것이 아니라 요청을 처리하는 Servlet 쓰레드가 요청후 바로 반환되고 내부의 다른 쓰레드가 이를 처리했다가 처리완료 후 응답처리 리소스가 가용할 때 Servlet쓰레드가 응답처리를 계속 이어가게 해 주는 것이다.

비동기 요청처리를 위해 다음과 같은 환경이 필요하다.

  • Servlet 3.0이상
  • Spring MVC 3.2이상 (eGov 3.0에 포함)
  • Web.xml 설정 변경

Servlet 3.0을 위한 pom dependency변경

<!-- Servlet -->
<dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>javax.servlet-api</artifactId>
   <version>3.0.1</version>
   <scope>provided</scope>
</dependency>

web.xml설정 변경

web.xml의 servlet버전을 변경해야한다.

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">
 
    ...
 
</web-app>

web.xml내의 servlet설정에 async-supported 태그 값을 true로 설정한다.

<servlet>
   <servlet-name>appServlet</servlet-name>
   <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
   <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
   </init-param>
   <load-on-startup>1</load-on-startup>
   <async-supported>true</async-supported>
</servlet>

설명

Spring의 비동기 요청처리에서는 Servlet 쓰레드는 요청을 처리하고 Spring의 Async 제공 클래스를 통해 비동기모드로 전환되며 Servlet 쓰레드는 반환된다. 그 후 내부 Application의 쓰레드에서 서비스 처리가 일어나게 된다. 처리완료 후 Servlet 쓰레드가 다시 응답을 받아 이를 클라이언트로 전송한다.

Spring의 비동기 요청처리에서 제공하는 Class는 Servlet 쓰레드가 반환된 이후 내부 서비스를 처리하는 쓰레드의 종류에 따라 나뉜다.

Callable

요청을 처리하는 Servlet 쓰레드가 반환되면 Spring MVC에서 제어하는 쓰레드에 의해 비동기처리된다.

Callable의 처리과정은 다음과 같다.

  1. Controller의 RequestMapping method에서 일반적인 뷰 객체나 String값을 리턴하는 것이 아니라 Callable객체를 리턴한다.
  2. Callable이 리턴될 때 Servlet 쓰레드가 반환되며 비동기처리를 Callable에게 위임한다.
  3. Spring MVC내의 TaskExcutor에서 관리되는 쓰레드에서 Async처리가 된다.
  4. Callable내부의 call()함수에서 리턴되는 값이 다시 Servlet 쓰레드로 전달된다.

Callable은 주로 요청처리가 오래걸리는 DB작업, REST API 요청처리를 하는 데 적합하다.

@RequestMapping(/view)
public Callable<String> callableWithView(final Model model) {
  return new Callable<String>() {
    @Override
    public String call() throws Exception {
        Thread.sleep(2000);
        model.addAttribute(foo, bar);
        model.addAttribute(fruit, apple);
        return view;
    }
}

DefferedResult

Servlet 스레드는 반환하고 Spring MVC가 제어하지 않는 쓰레드를 통해 비동기를 처리한다. DefferedResult는 JMS, AMQP, 스케쥴러, Redis, 다른 HTTP요청에서 사용된다.

DefferedResult의 처리과정은 다음과 같다.

  1. Controller에서 DefferedResult를 반환하고 In-Memory Queue또는 List에 DefferedResult를 저장한다.
  2. Servlet 쓰레드는 반환되고 이벤트 발생 시 Queue에서 DefferedResult객체를 꺼내 사용한다.
@RequestMapping("/quotes")
@ResponseBody
public DeferredResult<String> quotes() {
  DeferredResult<String> deferredResult = new DeferredResult<String>();
  // Save the deferredResult in in-memory queue ...
  queue.add(defferedResult);
 
  return deferredResult;
}
 
// In some other thread...
@RequestMapping("/someEvent")
@ResponseBody
public String someEvent(String data) {
  for(DefferedResult<String> result : queue) {
    result.setResult(data);
  }
 
  return "view";
}

AsyncTask

Callable과 동일한 방식으로 사용하며 Controller에서 Callable을 담아서 반환한다.

Timeout을 추가할 수 있으며 AsyncTaskExecutor를 지정하거나 작업의 종류에 따라 쓰레드 풀을 분리하여 사용할 수 있다.

@RequestMapping("/facebooklink")
public WebAsyncTask<String> facebooklink() {
  return new WebAsyncTask<String>(
    30000L, // Timeout
    "facebookTaskExecutor", // TaskExecutor
    new Callable<String>() {
      @Override
      public String call() throws Exception {
      // 작업
        return result;
      }
    }
  );
}

참고자료

3.25 - jQuery 가이드

jQuery는 다양한 UI 기능과 AJAX 요청을 지원하는 JavaScript 라이브러리로, 이를 통해 이벤트 처리, 자동 완성, 탭 등의 기능을 쉽게 구현할 수 있다. AJAX 요청은 $.ajax(), $.get(), $.post()와 같은 메서드를 사용하여 서버와 데이터를 비동기적으로 통신하며, JSON과 같은 형식으로 데이터를 주고받을 수 있다.

jQuery 가이드

개요

jQuery는 브라우저 호환성이 있는 다양한 기능을 제공하는 자바스크립트 라이브러리이다. jQuery에서 제공하는 오픈 라이브러리들을 통해 java script로 ajax, event, 다양한 ui 기능 등을 구현할 수 있으며 위키가이드에서는 jQuery의 기본적인 몇가지 기능(ajax, callback함수, post호출 등)에 대하여 살펴본다.

자세한 내용은 jQuery 사이트를 살펴보도록 한다.

jQuery ajax의 다양한 기능들 중 기본Ajax기능과 응용을 통한 콤보박스, Select박스의 간단한 화면처리에 대하여 가이드한다.

설정

jQuery를 이용하기 위해서는 jQuery java script를 추가해주어야 한다. 추가하는 방법은 jquery url을 직접 명시하는 경우, 프로젝트에 jquery java script를 직접 추가하여 참조하는 경우가 있다.

  • jQuery url을 직접 명시하는 경우
<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
  • jQuery script를 직접 추가하여 참조하는 경우
<script type="text/javascript" src="jQuery파일 경로"></script>

jquery-버전.js를 다운받아 프로젝트 하위경로에 추가한 후, 저장한 경로를 적어준다.

jQuery AJAX의 기본 기능

jQuery.ajax()

jQuery Ajax기능을 위해서는 기본적으로 jQuery.ajax(url[,settings]) 함수를 이용한다.

ajax함수 안의 settings

jQuery ajax함수 안에는 다음과 같은 설정들이 가능하다.

설정설명defaulttype
urlrequest를 전달할 url명N/Aurl string
datarequest에 담아 전달할 data명과 data값N/AString/Plain Object/Array
contentTypeserver로 데이터를 전달할 때 contentType‘application/x-www-form-urlencoded; charset=UTF-8’contentType String
dataType서버로부터 전달받을 데이터 타입xml, json, script, or htmlxml/html/script/json/jsonp/multiple, space-separated values
statusCodeHTTP 상태코드에 따라 분기처리되는 함수N/A상태코드로 분리되는 함수
beforeSendrequest가 서버로 전달되기 전에 호출되는 콜백함수N/AFunction( jqXHR jqXHR, PlainObject settings )
error요청을 실패할 경우 호출되는 함수N/AFunction( jqXHR jqXHR, String textStatus, String errorThrown )
success요청에 성공할 경우 호출되는 함수N/AFunction( PlainObject data, String textStatus, jqXHR jqXHR )
crossDomaincrossDomain request(jsonP와 같은)를 강제할 때 설정(cross-domain request설정 필요)same-domain request에서 false, cross-domain request에서는 trueBoolean
예제 1

하나의 파라미터를 ajax request로 전달하는 예제는 다음과 같다. example01.do로 호출을 하며 sampleInput이란 데이터명으로 “sampleData” String을 전달한다. 요청이 성공할 경우 success의 함수를 호출하며 실패시 error함수를 호출하는 자바스크립트 코드이다.

$.ajax({
   url : "<c:url value='/example01.do'/>",
   data : {
    sampleInput : "sampleData"
   },
   success : function(data, textStatus, jqXHR) {
    //Sucess시, 처리
   },
   error : function(jqXHR, textStatus, errorThrown){
    //Error시, 처리
}
});

ajax함수의 callback함수

ajax의 콜백함수이다. jQuery 1.5부터 jQuery의 모든 Ajax함수는 XMLHttpRequest객체의 상위 집합을 리턴받을 수 있게 되었다. 이 객체를 jQuery에서는 jqXHR이라 부르며, jqXHR의 함수로 콜백함수를 정의할 수 있다.

아래 콜백함수와 위의 settings에서 정의된 error, success콜백함수와 다른 점은 다음과 같다.

  • 사용자 정의에 의해 순차적으로 실행된다.
  • ajax에서 request를 리턴받아 호출할 수 있다.
함수명설명
jqXHR.done(function( data, textStatus, jqXHR ) {});성공시 호출되는 콜백함수
jqXHR.fail(function( jqXHR, textStatus, errorThrown ) {});실패시 호출되는 콜백함수
jqXHR.always(function( data|jqXHR, textStatus, jqXHR'|errorThrown ) { });항상 호출되는 콜백함수
예제 2

여러개의 데이터를 전달하며 호출 후 콜백함수로 서버에서 값을 받는 예제이다. example02.do을 호출하며 name, location을 요청데이터로 전달한다. 성공시에 done콜백함수를 호출한다.

$.ajax({
   url : "<c:url value='/example02.do'/>",
   data : {
      name : "gil-dong",
      location : "seoul"
   },
})
    .done(function( data ) {
   if ( console && console.log ) {
    console.log( "Sample of data:", data.slice( 0, 100 ) );
   }
});
예제 3

example03.do를 호출하며 성공시 done콜백함수를, 실패시 fail콜백함수가 호출된다. 성공,실패여부에 상관없이 always콜백함수는 항상 호출된다. done, fail, always콜백함수는 ajax함수를 통해 리턴되 request로 호출가능하다.

var jqxhr = $.ajax( "<c:url value='/example03.do'/>", )
    .done(function() {
        alert( "success" );
    })
    .fail(function() {
        alert( "error" );
    })
    .always(function() {
        alert( "complete" );
    });

jqxhr.always(function() {
    alert( "second complete" );
});

jQuery.get()

jQuery 1.5부터 sucess콜백함수는 jqXHR(XMLHttpRequest의 상위집합 객체)를 받을 수 있게 되었다. 그러나 JSONP와 같은 cross-domain request의 GET요청 시에는 jqXHR을 사용하여도 XHR인자는 success함수 안에서 undefined로 인식된다.

jQuery.get()은 ajax를 GET요청하는 함수이며 jqXHR을 반환받는다. 따라서 $.ajax()와 동일하게 done, fail, always콜백함수를 쓸 수 있다. get함수는 ajax함수로 나타내면 다음과 같다.

$.ajax({
   url: url,
   data: data,
   success: success,
   dataType: dataType
});

get함수 설정

설정설명defaulttype
urlrequest를 전달할 url명N/Aurl String
datarequest에 담아 전달할 data명과 data값N/AString/Plain Object
dataType서버로부터 전달받을 데이터 타입xml, json, script, or htmlString
success요청에 성공할 경우 호출되는 함수N/AFunction( PlainObject data, String textStatus, jqXHR jqXHR )
예제 1

url만 호출하고 결과값은 무시하는 경우

$.get( "example.do" );
예제 2

url로 데이터만 보내고 결과는 무시하는 경우

$.get( "example.do", { name: "gil-dong", location: "seoul" } );
예제 3

url를 호출하고 결과값을 Alert창으로 띄우는 경우

$.get( "test.php", function( data ) {
    alert( "Data Loaded: " + data );
});
예제 4

url를 호출하고 결과값을 Alert창으로 띄우는 경우

$.get( "example.do", { name: "gil-dong", location: "seoul" } )
    .done(function( data ) {
        alert( "Data Loaded: " + data );
});

jQuery.getJSON()

ajax호출을 HTTP GET메서드로 JSON문자열로 인코딩한 데이터를 요청한다. $.ajax()메서드로 표현하면 다음과 같다.

$.ajax({
    url: url,
    data: data,
    success: success,
    dataType: 'json'
});

getJSON함수 설정

설정설명defaulttype
urlrequest를 전달할 url명N/Aurl String
datarequest에 담아 전달할 data명과 data값N/AString/Plain Object
dataType서버로부터 전달받을 데이터 타입xml, json, script, or htmlString

jQuery.post()

jQuery.post()은 ajax를 POST요청하는 함수이며 jqXHR을 반환받는다. 따라서 ajax(), get()와 동일하게 done, fail, always콜백함수를 쓸 수 있다. jQuery.post 함수 설정은 get함수와 동일하다.(jQuery.post( url [, data ] [, success ] [, dataType ] ))

jQuery.post함수를 ajax함수로 쓰면 다음과 같다.

$.ajax({
    type: "POST",
    url: url,
    data: data,
    success: success,
    dataType: dataType
});

예제 1

url만 호출하고 결과값은 무시하는 경우

$.post( "example.do" );

예제 2

url로 데이터만 보내고 결과는 무시하는 경우

$.post( "example.do", { name: "gil-dong", location: "seoul" } );

예제 3

url를 호출하고 결과값을 console log를 남기는 경우

$.post( "example.do", function( data ) {
    console.log( data.name );
    console.log( data.location );
});

예제 4

url로 데이터를 호출하고 결과값을 Alert창으로 띄우는 경우

$.post( "example.do", { name: "gil-dong", location: "seoul" } )
    .done(function( data ) {
        alert( "Data Loaded: " + data );
});

jQuery Ajax 응용

jQuery의 ajax 추가 UI기능(자동완성기능, 판넬 탭 등)을 사용하기 위해서는 jQuery UI를 설정해주어야 한다. 다음에서는 jQuery UI와 ajax를 이용한 자동완성기능(autocomplete), 판넬 탭(tabs)에 대하여 가이드한다.

jQuery UI 설정

jQuery UI(version 1.11.0)을 추가하기 위해서는 위에서 설정했던 기본 jQuery script와 jQuery ui스크립트를 다음과 같이 jsp에 추가해준다.

...
<link rel="stylesheet"
	href="http://code.jquery.com/ui/1.11.0/themes/smoothness/jquery-ui.css" />
<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
<script src="http://code.jquery.com/ui/1.11.0/jquery-ui.js"></script>
...

jQuery UI script를 직접 추가하여 참조하는 경우는 jQuery-ui.js와 jQuery-ui.css를 다운받아 프로젝트 하위 경로에 추가한 후, 저장한 경로를 지정해준다.

Auto complete

jQuery에서는 input창에서 예상되는 텍스트값을 보여주는 자동완성기능을 쉽게 구현할 수 있도록 autoComplete()을 제공하고 있다.

autoComplete의 설정

구분설정설명Type
Optionssource하단에 뜨는 자동완성리스트(필수값)Array, String, function
OptionsminLength자동완성이 동작하는 최소 문자열 수Integer
Optionsdisableddisable 여부Boolean
Eventschange(event, ui)값 변경시 발생하는 이벤트 함수autocompletechange
Eventsfocus( event, ui )값이 포커스될 때 발생하는 이벤트 함수autocompletefocus
Eventsselect( event, ui )값이 선택될 때 발생하는 이벤트 함수autocompleteselect

자세한 내용은 jQuery 사이트의 autocomplete api을 참고한다.

autoComplete 기본 예제

기본 autoComplete기능 구현은 다음과 같다.

<html lang="en">
<head>
  <meta charset="utf-8">
  <title>jQuery UI Autocomplete - Default functionality</title>
  <link rel="stylesheet" href="//code.jquery.com/ui/1.11.0/themes/smoothness/jquery-ui.css">
  <script src="//code.jquery.com/jquery-1.10.2.js"></script>
  <script src="//code.jquery.com/ui/1.11.0/jquery-ui.js"></script>
  <link rel="stylesheet" href="/resources/demos/style.css">
  <script>
  $(function() {
    var availableTags = [ "ActionScript", "AppleScript", "Asp", "BASIC",
				"C", "C++", "Clojure", "COBOL", "ColdFusion", "Erlang",
				"Fortran", "Groovy", "Haskell", "Java", "JavaScript", "Lisp",
				"Perl", "PHP", "Python", "Ruby", "Scala", "Scheme" ];
    $( "#tags" ).autocomplete({
      source: availableTags
    });
  });
  </script>
</head>
<body>

<div class="ui-widget">
  <label for="tags">Tags: </label>
  <input id="tags">
</div>
</body>
</html>

위와 같이 했을 때 결과는 다음과 같다.

simpleautocomplete

minLength는 default값이 1이기 때문에 input에 1개이상의 문자를 입력했을 때 source의 String배열들이 자동문자리스트로 뜨게 된다.

autoComplete와 ajax를 이용한 응용예제

ajax를 통해 리스트를 받아와 autoComplete의 source로 뿌려주는 예제에 대해 살펴보자.
ajax를 통해 source를 가져오기 위해서는 서버호출 결과값이 2가지 중 하나의 타입이어야 한다.

  • String array타입
  • Object(id, label,value 값을 갖는) array타입
  1. String array로 가져오는 경우 example.do라는 url로 ajax호출을 통해 source를 가져오고 선택 시 값이 alert되도록 하는 예제이다.
...
<script type="text/javaScript">
$(function() {
   $('#autoValue').autocomplete(
      {
         source : function(request, response) {
	    $.ajax({
	       url : "<c:url value='/example.do'/>",
	       data : { input : request.term },
               success : function(data) {
	          response( data.locations );
	       }
	   });
         },
	 minLength : 1,
	 select : function(event, ui) {
            alert( ui.item ? "Selected : " + ui.item.label
		: "Nothing select, input was " + this.value);
	 }
     });
});
</script>
//... 생략
<input type="text" id="autoValue" />
//... 생략

data로 전송하는 request.term은 input값으로 사용자가 입력한 값이다. 즉, 사용자가 “je”를 입력하면 input = je 로 값이 넘어간다.
이 때 Controller에서는 reqeust.getParameter(“input”) 또는 @RequestParam(“input”) String input으로 값을 꺼낼 수 있다.

다음은 MappingJacksonJsonView의 빈을 jsonView로 등록했을 때 Controller에서 data를 꺼내고 결과값을 client로 넘겨주는 예제이다.(Ajax통신 시 java코드는 <Ajax support java code 위키>를 참고한다.)

@RequestMapping(value="/autoList.do")
public String autoList(HttpServletRequest request, ModelMap model) {

	String input = request.getParameter("input");
	List<String> resultList = new ArrayList<String>();
        //...생략...
	//서비스클래스를 통해 결과값을 resultList에 담음		
	model.addAttribute("locations", resultList );
 
	return "jsonView";
}

이 때, 호출되는 Query문의 예이다. (mybatis예제)

<select id="selectLocationList" parameterType="string" resultType="string">
SELECT
	LOCATION_NM
FROM LOCATION
	WHERE upper(LOCATION_NM) LIKE '%' || upper(#{input}) || '%'
</select>

만약 결과값이 다음과 같다면

{"locations":["Daejeon","Jeju-do","Jeolabuk-do","Jeolanam-do"]}

ajax의 성공시 콜백함수인 success에서는 data.locations로 값을 꺼내 autocomplete의 source를 설정할 수 있다.
위와 같은 경우 결과 화면은 다음과 같다.

autocomplete02

  1. Object array로 가져오는 경우

위와 동일하게 Query문을 통해 입력값에 따라 데이터를 검색하고 Object array로 서버에서 결과값을 가져오는 경우에 대해 가이드한다.
Controller에서 List의 값을 ModelMap에 “locations”라는 이름으로 클라이언트로 넘겨주고 서버에서 다음과 같이 json data가 넘어왔을 때

{"locations":[{"locationId":"0006","locationNm":"Daejeon","localNb":"042"},{"locationId":"0010","locationNm":"Jeju-do","localNb":"064"},{"locationId":"0011","locationNm":"Jeolabuk-do","localNb":"063"},{"locationId":"0012","locationNm":"Jeolanam-do","localNb":"061"}]}

autocomplete의 source에 넘어온 값이 나타나도록 하는 Object는 label, id, value값을 가질 수 있다. 그렇기 때문에 넘어온 request의 값을 success콜백함수에서 source에 나타나도록하는 Object형태로 변환해야 한다.
나머지 jQuery구현은 동일하다.

success : function(data) {
    response($.map(data.locations, function(item) {
        return{
            id: item.locationId,
            label: item.locationNm,
            //value: item.localNb
        }));
    }

이 때, 자동완성 리스트로 나타나는 값은 label이며, value를 설정해주었을때는 자동완성 리스트에서 값 선택 시 input값에 label값이 아닌 value값이 대입된다.

Select box

selectbox(combobox) 제어

jQuery에서는 별도로 select box ui함수를 제공하지 않는다.
jQuery를 통해 selectbox를 제어하는 방법에 대하여 알아보고 selectbox에 나타나는 리스트를 ajax로 구현하는 방법에 대하여 살펴본다.
다루고자 하는 selectbox가 다음과 같다고 가정하자.

<select id="combobox">
   <option value="">===locations===</option>
   <option value="01">Seoul</option>
   <option value="02">Busan</option>
   <option value="03">Jeju-do</option>
   <option value="04">Incheon</option>
</select>

selectbox 값 가져오기

selectbox에서 선택된 value를 가져오는 방법은 다음과 같다.

var selectedVal = $("#combobox option:selected").val();

selectbox 내용 가져오기

selectbox에서 선택된 text(ex:Seoul)를 가져오는 방법은 다음과 같다.

var selectedText= $("#combobox option:selected").text();

selectbox에서 선택된 Index값 가져오기

selectbox의 리스트에서 선택된 Index를 구하는 방법은 다음과 같다.

var selectedIndex = $("#combobox option").index($("#combobox option:selected"));

selectbox에 리스트 마지막에 추가하기

selectbox의 리스트에 값을 마지막에 추가하는 방법은 다음과 같다.

$("#combobox").append("<option value="05">Daejeon</option>");

맨 앞에 추가하는 경우는 .prepend()를 쓴다.

selectbox 값 교체하기

selectbox의 리스트의 값들을 교체하는 방법은 다음과 같다.

$("#combobox").html(
"<option value=''>===locations===</option>
   <option value='01'>Jeju-do</option>
   <option value='02'>Seoul</option>
   <option value='03'>Incheon</option>
   <option value='04'>Daejeon</option>")

selectbox 값 교체하기

selectbox의 리스트의 값들을 교체하는 방법은 다음과 같다.

$("#combobox").html(
"<option value=''>===locations===</option>
   <option value='01'>Jeju-do</option>
   <option value='02">Seoul</option>
   <option value='03'>Incheon</option>
   <option value='04'>Daejeon</option>")

selectbox에서 값 삭제하기

selectbox의 리스트에서 선택된 값을 삭제하는 방법은 다음과 같다.

$("#combobox option:selected").remove();

selectbox에서 값이 선택될 때 콜백함수

selectbox에서 값이 선택되었을 때 호출되는 콜백함수는 다음과 같다.

$("#combobox).change(function() {
    //기능구현
});

selectbox 만들기

위에서 쓴 selectbox 제어기능과 jQuery ajax함수를 이용하여 다음과 같이 간단히 selectbox를 만들 수 있다.

<JSP에서 콤보박스 구현 예제>

<script type="text/javaScript">
	$(function() {
		$.ajax({
			url : "<c:url value='/simpleCombo.do'/>",
			success : function(data) {
				loadCombo($("#combobox"), data.locations);
                                $("#combobox").val("");
			}
		});
 
		$("#combobox").change(function() {
			alert("Selected : " + $("#combobox option:selected").val());
		});
	});
 
	function loadCombo(target, data) {
		var dataArr = [];
		var inx = 0;
		target.empty();
 
		$(data).each( function() {
			dataArr[inx++] = "<option value=" + this.locationId + ">" + this.locationNm + "</option> ";
		});
 
		target.append(dataArr);
	}
</script>

//... 생략
<select id="combobox">
    <option>===locations===</option>
</select>

<서버에서 가져오는 결과값>

{"locations":[{"locationId":"0001","locationNm":"Seoul"},{"locationId":"0002","locationNm":"Busan"},{"locationId":"0003","locationNm":"Daegu"},{"locationId":"0004","locationNm":"Gwangju"},{"locationId":"0005","locationNm":"Incheon"},{"locationId":"0006","locationNm":"Daejeon"},{"locationId":"0007","locationNm":"Ulsan"},{"locationId":"0008","locationNm":"Gyeonggi-do"},{"locationId":"0009","locationNm":"Gangwon-do"},{"locationId":"0010","locationNm":"Jeju-do"},{"locationId":"0011","locationNm":"Jeolabuk-do"},{"locationId":"0012","locationNm":"Jeolanam-do"}]}

ajax함수가 실행되면서 simpleCombo.do를 통해 data를 가져오고 json값이 위와 같을 때 combobox를 구성하는 함수를 구현하여 combobox에 나오는 목록을 나타낼 수 있다. 위의 결과는 다음과 같다.

selectbox

Tabs

Tab 기본 구현하기 jQuery UI에서 tab구현은 tabs()를 쓰며 추가 설정은 다음과 같다.

설정설명구분
active활성화될 panel선택options
eventtab이 활성화되는 이벤트options
hidepanel이 숨겨질 때 애니메이션options
showpanel이 나타날 때 애니메이션options
beforeLoadRemote tab이 로드되기 전에 실행되기 전에 발생되는 이벤트 함수events
createevents

Tab을 구현하는 경우 기본 예제는 다음과 같다.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>jQuery UI Tabs - Default functionality</title>
  <link rel="stylesheet" href="//code.jquery.com/ui/1.11.0/themes/smoothness/jquery-ui.css">
  <script src="//code.jquery.com/jquery-1.10.2.js"></script>
  <script src="//code.jquery.com/ui/1.11.0/jquery-ui.js"></script>
  <link rel="stylesheet" href="/resources/demos/style.css">
  <script>
  $(function() {
    $( "#tabs" ).tabs();
  });
  </script>
</head>
<body>

<div id="tabs">
  <ul>
    <li><a href="#tabs-1">Nunc tincidunt</a></li>
    <li><a href="#tabs-2">Proin dolor</a></li>
    <li><a href="#tabs-3">Aenean lacinia</a></li>
  </ul>
  <div id="tabs-1">
    <p>Proin elit arcu, rutrum commodo, vehicula tempus, commodo a, risus. Curabitur nec arcu. Donec sollicitudin mi sit amet mauris. Nam elementum quam ullamcorper ante. Etiam aliquet massa et lorem. Mauris dapibus lacus auctor risus. Aenean tempor ullamcorper leo. Vivamus sed magna quis ligula eleifend adipiscing. Duis orci. Aliquam sodales tortor vitae ipsum. Aliquam nulla. Duis aliquam molestie erat. Ut et mauris vel pede varius sollicitudin. Sed ut dolor nec orci tincidunt interdum. Phasellus ipsum. Nunc tristique tempus lectus.</p>
  </div>
  <div id="tabs-2">
    <p>Morbi tincidunt, dui sit amet facilisis feugiat, odio metus gravida ante, ut pharetra massa metus id nunc. Duis scelerisque molestie turpis. Sed fringilla, massa eget luctus malesuada, metus eros molestie lectus, ut tempus eros massa ut dolor. Aenean aliquet fringilla sem. Suspendisse sed ligula in ligula suscipit aliquam. Praesent in eros vestibulum mi adipiscing adipiscing. Morbi facilisis. Curabitur ornare consequat nunc. Aenean vel metus. Ut posuere viverra nulla. Aliquam erat volutpat. Pellentesque convallis. Maecenas feugiat, tellus pellentesque pretium posuere, felis lorem euismod felis, eu ornare leo nisi vel felis. Mauris consectetur tortor et purus.</p>
  </div>
  <div id="tabs-3">
    <p>Mauris eleifend est et turpis. Duis id erat. Suspendisse potenti. Aliquam vulputate, pede vel vehicula accumsan, mi neque rutrum erat, eu congue orci lorem eget lorem. Vestibulum non ante. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Fusce sodales. Quisque eu urna vel enim commodo pellentesque. Praesent eu risus hendrerit ligula tempus pretium. Curabitur lorem enim, pretium nec, feugiat nec, luctus a, lacus.</p>
    <p>Duis cursus. Maecenas ligula eros, blandit nec, pharetra at, semper at, magna. Nullam ac lacus. Nulla facilisi. Praesent viverra justo vitae neque. Praesent blandit adipiscing velit. Suspendisse potenti. Donec mattis, pede vel pharetra blandit, magna ligula faucibus eros, id euismod lacus dolor eget odio. Nam scelerisque. Donec non libero sed nulla mattis commodo. Ut sagittis. Donec nisi lectus, feugiat porttitor, tempor ac, tempor vitae, pede. Aenean vehicula velit eu tellus interdum rutrum. Maecenas commodo. Pellentesque nec elit. Fusce in lacus. Vivamus a libero vitae lectus hendrerit hendrerit.</p>
  </div>
</div>
</body>
</html>

위의 결과는 다음과 같다.

panel

ajax로 Tab 구현하기 ajax로 Tab을 구현하기 위해서는 각 Tab에 ajax호출 url을 지정해주기만 하면 된다.

<script src="http://code.jquery.com/jquery-1.10.2.js"></script>
<script src="http://code.jquery.com/ui/1.11.0/jquery-ui.js"></script>
<script type="text/javaScript">
$(function() {
    $( "#tabs" ).tabs({
      beforeLoad: function( event, ui ) {
        ui.jqXHR.error(function() {
          ui.panel.html(
            "Couldn't load this tab. We'll try to fix this as soon as possible. " +
            "If this wouldn't be a demo." );
        });
      }
    });
  });
</script>
</head>
<body>
   <div id="tabs">
      <ul>
         <li><a href="${pageContext.request.contextPath }/simpleCombo.do">Tab 1</a></li>
	 <li><a href="${pageContext.request.contextPath }/tabTwoForm.do">Tab 2</a></li>
	 <li><a href="${pageContext.request.contextPath }/tabThreeForm.do">Tab 3</a></li>
      </ul>
   </div>
</body>

위의 경우 첫번째 탭 지정시 simpleCombo.do가 호출되어 panel안에 simpleCombo.do를 통해 호출된 화면이 보여진다.

참고자료

3.26 - WebSocket

WebSocket은 HTTP 환경에서 양방향 통신을 지원하는 Spring 기술로, Spring은 기본적으로 STOMP sub-protocol을 사용한다. Spring Framework 4.0부터 spring-websocket 모듈이 추가되어 복잡한 WebSocket 통신을 지원하며, JSR356(Java WebSocket API)과 호환된다.

WebSocket

개요

WebSocket 은 HTTP 환경에서 소켓 통신을 지원하기 위한 Spring 기술이다. Spring 은 기본적으로 WebSocket sub-protocol 로 STOMP 를 사용한다.

(RFC6455는 웹 어플리케이션을 위한 새 기능으로 WebSocket protocol을 정의한다. 서버와 클라이언트간 양방향 통신(full-duplex)을 지원하는데, 이것은 웹을 좀 더 인터랙티브하게 만들기 위해 사용하였던 java applet, XMLHttpRequest, Flash, ActiveX 등의 기술을 대체하기 위한 중요한 기능이 될 수 있다.)

HTTP 는 초기 handshake (protocol upgrade or switch 요청이고 서버가 동의하는 경우 101 응답을 내려 줌)를 위해서만 사용되며, handshake 가 성공하면 HTTP upgrade 요청에 기인하는 TCP 소켓이 open 된 채 서버와 클라이언트간 통신을 처리한다.

  • Spring Framework 4.0 에서는 새로운 spring-websocket module 을 포함하며 이를 통해 복잡한 WebSocket 을 지원하고, 이 모듈은 Java WebSocket API (JSR356) 과 호환되며 추가적인 기능을 제공한다.

Spring Framework 4.0 WebSocket 지원 기능

WebSocket을 위한 요구사항.

  • Java EE 7, JDK 7
  • Servlet 3.1 ( Container : Apache Tomcat 7.0.47+, Eclipse Jetty 9.0+, GlassFish 4.0+ )
  • Spring MVC 4.0이상 (eGov 3.5에 포함)
  • websocket을 지원하는 브라우저 websocket 브라우저 테스트 페이지

1. WebSocket Fallback Options

WebSocket은 Servlet 3.1에서 제공하기 시작한 기능을 이용하여 구현된 기술로서 모든 브라우저가 WebSocket을 지원하지는 않는다(IE는 10부터 지원). 따라서 WebSocket을 지원하지 않는 브라우저의 경우 해당 기능을 사용할 수 없고, 몇몇 Proxy 에서는 긴 연결상태를 강제로 끊어버리는 등의 오작동이 있을 수 있다. (관련 대안으로 fallback option을 지원하도록 구성된 Spring framework의 SockJS protocol 참고 - Spring 설정을 활성화 시킴으로서 쉽게 Application 에 적용 가능)

2. Messaging Architecture

기존에 구성하던 방식의 개발방법인 REST는 Web application 개발에 있어 많은 URL (noun) 과 몇개의 HTTP method (verbs), 그리고 상태와 무관한 architecture 를 사용한다.

WebSocket application 에서는 초기 HTTP handshake 에서만 하나의 URL 을 사용하고, 그 후의 모든 메시지는 handshake 시 맺어진 TCP 연결을 통해서 전송된다. 이것은 전통적인 messging application (JMS, AMQP) 과 가까운 비동기, event-driven, messaging 아키텍처를 사용하는 것이다.

Spring Framework 4.0 에서는 spring-messging 모듈을 통해 기능을 제공하는데, 이 모듈은 스프링 통합 프로젝트에서 사용하는 Message, MessageChannel, MessageHandler 와 같은 추상화된 개념을 제공하여 messaging archtecture 의 근간이 된다. 이 모듈은 Spring MVC annotaion 기반의 programming model 과 유사하게 작성할 수 있도록 몇 가지 annotation 을 포함하고 있다.

3. Sub-Protocol Support in WebSocket

WebSocket 은 messaging architecture 를 함축하지만 특정 messaging protocol 사용을 강제하지 않는다. 이것은 매우 얇은 layer 이며 단순히 TCP 위에서 일련의 byte 스트림들을 메시지로 변환하는 것에 지나지 않는다. 메시지의 의미 해석은 Application 에 맡겨둔 채 말이다.

HTTP (이것은 application level protocol 이다.) 와 달리 WebSocket 의 protocol 은 단순화 하여 어떻게 처리할지 어디로 route 할지에 대한 충분한 정보가 제공되지 않는 저수준으로 제공된다. 이러한 이유로 WebSocket RFC 에서는 sub-protocol 들의 사용을 정의하였다. handshake 사용 중 클라이언트와 서버는 Sec-WebSocket-Protocol 헤더를 사용할 수 있고 이를 통해 서로 통신할 sub protocol 을 합의한다. WebSocket 은 sub-protocol 을 강제하지 않기에 필수적인 것은 아니지만 이 경우 client/server 는 서로 message 포맷에 대해 합의되어야 한다.

Spring 은 STOMP 지원을 제공하며 이는 HTTP 와 유사한 형태로 script 언어에서 사용을 위해 만들어진 것이다. STOMP 는 널리 지원되고 WebSocket 사용에 적합하다.

4. WebSocket 을 사용하여야 하는가?

WebSocket 은 서버와 클라이언트가 적은 지연에 매우 빈번하게 이벤트를 교환해야 할 경우에 적합하다. 이는 finance, game, collaboration 등의 application 일 수 있다. 이들은 모두 time delay 에 민감하고 매우 빈번하게 많은 종류의 메시지를 교환해야 한다.

social, news feed 와 같은 경우는 단순 polling 으로 충분하다. 이들은 latency 가 중요한 요소가 아니다. latency 가 중요하더라도 message 의 크기가 작다면 (network failure check 같은) long polling 이 충분한 대안이 될 수 있다.

low latency 와 high frequency 가 중요한 경우에는 WebSocket 이 적합하다. 하지만 이러한 경우에도 모든 client-server 통신이 WebSocket 을 이용해야 하는 지는 application 에 따라 다를 수 있다. 최적의 경우를 생각하여 client 가 사용할 수 있는 대안으로서 WebSocket 과 REST API 를 모두 제공해야 할 수도 있다. 이 경우 REST 호출을 통해 특정 메시지를 WebSocket 클라이언트들에게 모두 전달될 필요가 있을 수도 있다.

Spring Framework 는 @Controller, @RestController 클래스에 HTTP 핸들링 메소드와 WebSocket 핸들링 메소드를 사용할 수 있다. 게다가 Spring MVC 요청 핸들링 메소드는 모든 WebSocket 클라이언트에 메시지를 전달하거나 특정 사용자에게만 전달하는 것을 쉽게 제공한다.

WebSocket API

Spring Framework 는 다양한 WebSocket Engine 에 적합하게 설계되었다. 예를 들어 Tomcat (7.0.47+) or GlassFish (4.0+), WildFly (8.0+) JSR-356 런타임상에서 구동하거나 Jetty (9.1+) 에서와 같이 native WebSocket 지원 환경에서 구동할 수 있다.

직접적인 WebSocket API 를 application 개발에 사용하는 것은 매우 저수준의 행위까지 설계해야 한다. 메시지 포맷에 대한 것 또는 annotation 을 통한 message 라우팅 같은 것을 지원하도록 application 에서는 sub-protocol 을 사용하는 것이 좋으며, 이러한 맥락에서 Spring 은 STOMP over WebSocket 을 지원한다.

Spring WebSocket 지원 살펴보기

1. WebSocketHandler 설정

Spring 은 WebSocketHandler 를 구현함으로 WebSocket 서버를 만드는 것을 지원한다. WebSocketHandler 는 TextWebSocketHandler 나 BinaryWebSocketHandler 로 세분화 되어 있다.

import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;
 
public class MyHandler extends TextWebSocketHandler {
 
    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) {
        // ...
    }
 
}

특정 URL 에 WebSocketHandler 를 설정하는 Java-Config 설정

import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
 
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
 
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/myHandler");
    }
 
    @Bean
    public WebSocketHandler myHandler() {
        return new MyHandler();
    }
 
}

동일 설정을 위한 XML Config 설정

<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:websocket="http://www.springframework.org/schema/websocket" 
    xsi:schemaLocation=" 
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd">
 
    <websocket:handlers>
        <websocket:mapping path="/myHandler" handler="myHandler"/>
    </websocket:handlers>
 
    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>
 
</beans>

위의 설정은 Spring 의 DispatcherServlet 을 사용하는 설정이다. 하지만 Spring 은 다른 Web 개발 환경에서 WebSocketHandler 를 사용할 수 있도록 WebSocketHttpRequestHandler 를 지원한다.

2. WebSocket Handshaking 커스터마이즈

초기 WebSockethandshake 를 커스터마이징 하는 가장 손쉬운 방법은 HandshakeInterceptor 를 사용하는 것인데 이것은 handshake 하는 것에 대한 before 와 after 처리를 기술할 수 있다. 이것은 handshake 를 준비하기 위해 사용되거나 WebSocketSession 에서 특정 attribute 를 이용할 수 있도록 하는데 사용된다. 아래는 Spring 에서 제공되는 interceptor 로 http 세션 attribute 를 WebSocketSession 에 전달해 주는 일을 한다.

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
 
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new MyHandler(), "/myHandler")
            .addInterceptors(new HttpSessionHandshakeInterceptor());
    }
 
}
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:websocket="http://www.springframework.org/schema/websocket" 
    xsi:schemaLocation=" 
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd">
 
    <websocket:handlers>
        <websocket:mapping path="/myHandler" handler="myHandler"/>
        <websocket:handshake-interceptors>
            <bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
        </websocket:handshake-interceptors>
    </websocket:handlers>
 
    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>
 
</beans>

좀 더 고급 옵션을 적용하려면 DefaultHandshakeHandler 를 확장하여야 한다. 이것은 validating, client origin, sub-protocol 협의 및 기타 여러가지를 포함하고 있다. application 은 특정 WebSocket server engine 이나 아직 지원하지 않는 버전에 대한 지원을 위해서 커스텀 RequestUpgradeStrategy 를 설정할 필요가 있다.

3. WebSocketHandler Decoration

Spring 은 WebSocketHandlerDecorator 기본 클래스를 제공한다. 이것은 WebSocketHandler 에 추가적인 행위를 decorate 하는데 사용된다. Logging, Exception Handling WebSocketHandlerDecorator 구현이 Java-config 나 XML config 를 통해서 기본적으로 제공되고 있다. ExceptionWebSocketHandlerDecorator 는 WebSocketHandler 에서 발생하는 처리되지 않은 모든 예외를 catch 하여 Server error 를 나타내기 위해 1011 status code 로 응답한다.

4. 배포 고려사항

Spring WebSocket API 는 Spring MVC application 과 통합이 쉽다. 그것은 Dispatcher Servlet 이 HTTP WebSocket handshake 뿐만 아니라 다른 HTTP 요청도 서비스 하기 때문이다. WebSocketHttpRequestHandler 를 사용하면 다른 HTTP 처리 시나리오 (Spring MVC 이외의 다른 Web Framework 와 같은) 와도 쉽게 결합할 수 있다. 그러나 JSR-356 runtime 에 특별한 고려사항이 있다.

Java WebSocket API (JSR-356) 는 두가지 배포 메커니즘을 제공한다. 첫번째는 시작 시 Servlet container classpath scan (Servlet 3 의 기능) 에 관한 것이고 다른 것은 Servlet container 초기화에서 사용하기 위한 registration API 에 관한 것이다. 이것 중 어느 것도 모든 HTTP 요청을 처리하는 단일 front controller 를 사용하지 못한다. 즉 DispatcherServlet 은 원칙적으로 WebSocket handshake 와 다른 모든 HTTP 요청에 대해서 사용할 수 없다.

이것은 JSR-356 이 가지는 매우 중요한 한계인데 Spring 의 WebSocket 지원은 JSR-356 runtime 환경에서도 서버 특화의 RequestUpgradeStrategy 를 제공함으로 이를 지원하고 있다. 현재 Spring 은 Tomcat 7.0.47+, Jetty 9.1+, GlassFish4.0+, WildFly8.0+ 을 지원한다. 추가적인 지원은 더 많은 WebSocket runtime 을 이용할 수 있을 때 추가될 것이다.

두 번째 고려사항은 JSR-356 지원의 Servlet container 가 SCI scan 을 수행하도록 하고 있어 application 구동을 느리게 (혹은 어떤 특정 상황에서는 매우 느리게) 할 수 있다는 것이다. 만약 JSR-356 지원 Servlet container version 업그레이드시 명백한 영향이 목격된다면, 선택적으로 web fragments (SCI scanning) 을 활성 및 비활성할 수 있다. (이는 web.xml 의 absolute-ordering 을 사용토록 한다.) web fragment 순서를 명확히 하도록 web.xml 에서 absolute-ordering 을 사용 토록 한다. (이는 scanning 을 사용치 않도록 하는 방법이다.)

5. WebSocket 엔진 설정하기

각각의 기반이 되는 WebSocket engine 들은 message buffer size 나 idle timeout 등의 runtime 특성을 조절할 수 있는 configuration properties 를 가지고 있다. 이는 Tomcat, WildFly, Glassfish 에서ServletServerContainerFactoryBean 을 WebSocket java config 에 추가하여 설정할 수 있다.

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
 
    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxTextMessageBufferSize(8192);
        container.setMaxBinaryMessageBufferSize(8192);
        return container;
    }
 
}
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:websocket="http://www.springframework.org/schema/websocket" 
    xsi:schemaLocation=" 
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket.xsd">
 
    <bean class="org.springframework...ServletServerContainerFactoryBean">
        <property name="maxTextMessageBufferSize" value="8192"/>
        <property name="maxBinaryMessageBufferSize" value="8192"/>
    </bean>
 
</beans>

client 측 WebSocket 설정을 위해서는 WebSocketContainerFactoryBean(XML) 이나 ContainerProvider.getWebSocketContainer() 를 이용한다.

Jetty 를 사용하는 경우에는 미리 설정된 WebSocketServerFactory 를 필요로 하고 이를 WebSocket java config 를 통해서 Spring 의 DefaultHandshakeHandler 에 추가한다.

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
 
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(echoWebSocketHandler(),
            "/echo").setHandshakeHandler(handshakeHandler());
    }
 
    @Bean
    public DefaultHandshakeHandler handshakeHandler() {
 
        WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
        policy.setInputBufferSize(8192);
        policy.setIdleTimeout(600000);
 
        return new DefaultHandshakeHandler(
                new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
    }
 
}
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:websocket="http://www.springframework.org/schema/websocket" 
    xsi:schemaLocation=" 
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket.xsd">
 
    <websocket:handlers>
        <websocket:mapping path="/echo" handler="echoHandler"/>
        <websocket:handshake-handler ref="handshakeHandler"/>
    </websocket:handlers>
 
    <bean id="handshakeHandler" class="org.springframework...DefaultHandshakeHandler">
        <constructor-arg ref="upgradeStrategy"/>
    </bean>
 
    <bean id="upgradeStrategy" class="org.springframework...JettyRequestUpgradeStrategy">
        <constructor-arg ref="serverFactory"/>
    </bean>
 
    <bean id="serverFactory" class="org.eclipse.jetty...WebSocketServerFactory">
        <constructor-arg>
            <bean class="org.eclipse.jetty...WebSocketPolicy">
                <constructor-arg value="SERVER"/>
                <property name="inputBufferSize" value="8092"/>
                <property name="idleTimeout" value="600000"/>
            </bean>
        </constructor-arg>
    </bean>
 
</beans>

3.27 - STOMP over WebSocket 개요 및 메시지 처리 흐름

STOMP는 간단한 메시징 프로토콜로, Spring Framework에서 WebSocket과 결합해 메시지 전송 및 구독 기능을 제공한다. Spring WebSocket 설정을 통해 클라이언트와 서버 간의 메시지 흐름을 관리하며, 메시지를 처리하고 broadcasting하는 기능을 지원한다.

STOMP Over WebSocket Messaging Architecture

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp

WebSocket protocol 은 content 를 정의하지 않은 채 2 가지 유형 (text, binary) 의 메시지로 분류했다. content 를 정의하지 않은 대신 client 와 서버는 sub-protocol (content 를 정의하는 고수준의 protocol) 을 사용하는 것을 합의해야 할 수도 있다. sub-protocol 을 사용하는 것은 option 이지만 client 와 server 모두 메시지를 어떻게 해석해야 할지를 이해하는 것이 필요하다.

1. STOMP 개요

STOMP 는 Ruby, Python, Perl 과 같은 스크립트 언어를 위해 고안된 단순한 메시징 프로토콜이다. 그것은 메시징 프로토콜에서 일반적으로 사용되는 패턴들의 일부를 제공한다. STOMP 는 TCP 나 WebSocket 과 같은 신뢰성있는 양방향 streaming network protocol 상에 사용될 수 있다.

STOMP 는 HTTP 에 모델링된 frame 기반 프로토콜이다. 다음은 frame 의 구조이다.

COMMAND
header1:value1
header2:value2
 
Body^@

클라이언트는 메시지를 보내기 위해 SEND 명령을 사용하거나 수신 메시지에 관심을 표현하기 위해 SUBSCRIBE 명령을 사용할 수 있다. 이런 명령어들은 “destination” 헤더를 요구하는데 어디에 메시지를 전송할 지 혹은 어디에서 메시지를 구독할지를 나타낸다.

다음은 stock shares 를 구매하기 위한 요청 전송의 예이다.

SEND
destination:/queue/trade
content-type:application/json
content-length:44
 
{"action":"BUY","ticker":"MMM","shares",44}^@

다음은 stock quotes 를 얻기위한 클라이언트 구독의 예이다.

SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*
 
^@

destination 의 의미는 STOMP spec 에서 투명하게 남겨두었다. 그것은 어떤 문자열이든 될 수 있고 그 의미나 문법은 온전히 STOMP 서버에 맡겨진다. 그러나 일반적으로 다음의 규칙을 사용하곤 한다.

“topic/..” - publish-subscribe (one to many) “queue/” - point-to-point (one to one)

STOMP 서버는 모든 구독자에게 message 를 broadcasting 하기 위해 MESSAGE 명령을 사용할 수 있다. 다음은 stock quote 를 구독자에서 전달하는 예이다.

MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM
 
{"ticker":"MMM","price":129.45}^@

서버는 불분명한 메시지를 전송할 수 없음을 알아야 한다. 즉 서버의 모든 메시지는 특정 클라이언트 구독에 응답하여야 하고 서버 메시지의 “subscription-id” 헤더는 클라이언트 구독의 “id” 헤더와 일치하여야 한다.

지금까지 STOMP 의 가장 기본적인 이해를 위한 것이다. 상세한 것은 specification 을 통해 살펴볼 수 있다.

다음은 STOMP over WebSocket 사용 application 의 장점을 요약한 것이다.

  • Standard message format
  • Application-level protocol with support for common messaging patterns
  • Client-side support, e.g. stomp.js, msgs.js
  • The ability to interpret, route, and process messages on both client and server-side
  • The option to plug a message broker — RabbitMQ, ActiveMQ, many others — to broadcast messages (explained later)

순수 WebSocket 과 비교하여 STOMP 사용의 가장 중요한 요소는 Spring Framework 가 마치 SpringMVC 가 HTTP 에 프로그래밍 모델을 제공하는 것처럼 application 수준의 사용을 위한 프로그래밍 모델을 제공한다는 것이다.

2. Enable STOMP over WebSocket

Spring Framework 는 spring-messaging 와 spring-websocket 모듈을 통해 STOMP over WebSocket 사용을 제공한다.

다음은 SockJS fallback option 을 사용하는 STOMP WebSocket endpoint 설정의 예이다. endpoint 는 /app/portfolio URL 경로에 클라이언트가 접속할 수 있다.

import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
 
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
 
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app")
            .enableSimpleBroker("/queue", "/topic");
    }
 
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJS();
    }
 
    // ...
 
}

동일 XML 설정은 다음과 같다.

<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:websocket="http://www.springframework.org/schema/websocket" 
    xsi:schemaLocation=" 
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd">
 
    <websocket:message-broker application-destination-prefix="/app">
        <websocket:stomp-endpoint path="/portfolio">
            <websocket:sockjs/>
        </websocket:stomp-endpoint>
        <websocket:simple-broker prefix="/queue, /topic"/>
        ...
    </websocket:message-broker>
 
</beans>

브라우저 측에서는 stomp.js 나 sockjs-client 를 사용하여 접속한다.

var socket = new SockJS("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);
 
stompClient.connect({}, function(frame) {
}

SockJS 없는 WebSocket 사용은 다음과 같다.

var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);
 
stompClient.connect({}, function(frame) {
}

위의 stomp 클라이언트는 login 과 passcode 헤더를 정의할 필요가 없다. 제공되더라도 무시되거나 서버측에서 재정의 될 것이다. 인증에 대해서는 Section 20.4.8, “Connections To Full-Featured Broker”Section 20.4.9, “Authentication” 를 참고하라

3. Flow of Messages

STOMP endpoint 가 설정될 때, spring application 은 연결된 클라이언트들에게 마치 STOMP broker 인 것처럼 동작한다. 들어오는 메시지를 처리하고 다시 메시지를 전송한다. 이 장에서는 Application 내에서 message flow 가 어떠한지에 대한 개략적인 개요를 제공한다.

spring-messaging 모듈은 Spring Integration 프로젝트에 기반한 다양한 추상화를 포함하고 있으며, messging application 의 building block 으로 사용하도록 의도되었다.

  • Message  —  represents a message with headers and a payload.
  • MessageHandler — a contract for handling a message.
  • MessageChannel  —  a contract for sending a message enabling loose coupling between senders and receivers.
  • SubscribableChannel  —  extends MessageChannel and sends messages to registered MessageHandler subscribers.
  • ExecutorSubscribableChannel  —  a concrete implementation of SubscribableChannel that can deliver messages asynchronously through a thread pool.

제공되는 STOMP over WebSocket 설정 (Java,XML 모두) 은 다음의 3 가지 channel 들을 포함하여 실제적인 message flow 를 만들어내는데 사용된다.

  • “clientInboundChannel” — for messages from WebSocket clients. Every incoming WebSocket message carrying a STOMP frame is sent through this channel.
  • “clientOutboundChannel” — for messages to WebSocket clients. Every outgoing STOMP message from the broker is sent through this channel before getting sent to a client’s WebSocket session.
  • “brokerChannel” — for messages to the broker from within the application. Every message sent from the application to the broker passes through this channel.

“clientInboundChannel” 의 메시지는 처리 (요청 실행과 같은) 를 위해 annotated method 로 흐르거나 broker (구동과 같은) 에 포워딩 될 수 있다. STOMP destination 은 단순한 prefix 기반 라우팅을 위해 사용되어진다. 예를 들어 ”/app” prefix 는 annotated method 로 라우팅하고 ”/topic” 이나 ”/queue” 는 broker 에 라우팅되는 것이다.

메지시 처리 annotated method 가 return type 을 갖을 때는 그 return value 는 Spring Message 의 payload 로서 “brokerChannel” 에 전송된다. 그러면 broker 는 메시지를 client 들에게 broadcasting 한다. 메시지를 destination 에 전송하는 것은 messaging template 의 도움으로 application 내 어디에서나 수행될 수 있다. 예를 들어, HTTP POST 처리 메소드는 메시지를 연결된 클라이언트들에게 broadcast 할 수 있는 것이다. 또는 service component 는 주기적으로 stock quotes 를 broadcast 할 수 있다.

다음은 메시지 흐름을 보여주는 단순한 예이다.

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
 
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio");
    }
 
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.setApplicationDestinationPrefixes("/app");
        registry.enableSimpleBroker("/topic/");
    }
 
}
@Controller
public class GreetingController {
 
    @MessageMapping("/greeting") {
    public String handle(String greeting) {
        return "[" + getTimestamp() + ": " + greeting;
    }
 
}

다음은 위 예제의 메시지 흐름에 대한 설명이다.

  • WebSocket clients connect to the WebSocket endpoint at ”/portfolio”. * Subscriptions to ”/topic/greeting” pass through the “clientInboundChannel” and are forwarded to the broker.
  • Greetings sent to ”/app/greeting” pass through the “clientInboundChannel” and are forwarded to the GreetingController.
  • The controller adds the current time and the return value is passed through the “brokerChannel” as message to ”/topic/greeting” (destination is selected based on a convention but can be overridden via @SendTo).
  • The broker in turn broadcasts messages to subscribers and they pass through the “clientOutboundChannel”.

다음 장에서 argument 와 return value 의 종류를 포함한 annotated method 의 상세한 내용을 제공한다.

4. Annotation Message Handling

@MessageMapping annotation 은 @Controller 와 @RestController annotated class 의 메소드에 적용할 수 있다. 이것은 메소드를 path-like message destination 에 매핑하기 위해 사용될 수 있다. 이것은 또한 controller 내의 모든 annotated method 들과 공유되는 매핑을 표현하기 위해 type-level @MessageMapping 과 결합할 수 있다.

Destination mapping 은 Ant-style pattern (e.g. ”/foo*”, ”/foo/**”) 과 template variable (e.g. ”/foo/{id}”, 이것은 @DestinationVariable 메소드 인자를 통해 접근될 수 있다.) 을 포함할 수 있다. 이처럼 SpringMVC 사용자 친화적인데, 실질적으로 SpringMVC 가 그러하듯 AntPathMatcher 는 pattern 기반의 destination mapping 과 template variable 추출에 사용된다.

@MessageMapping 메소드에서 다음의 method 인자가 지원된다.

  • Message method argument to get access to the complete message being processed.
  • @Payload-annotated argument for access to the payload of a message, converted with a org.springframework.messaging.converter.MessageConverter. The presence of the annotation is not required since it is assumed by default. Payload method arguments annotated with Validation annotations (like @Validated) will be subject to JSR-303 validation.
  • @Header-annotated arguments for access to a specific header value along with type conversion using an org.springframework.core.convert.converter.Converter if necessary.
  • @Headers-annotated method argument that must also be assignable to java.util.Map for access to all headers in the message.
  • MessageHeaders method argument for getting access to a map of all headers.
  • MessageHeaderAccessor, SimpMessageHeaderAccessor, or StompHeaderAccessor for access to headers via typed accessor methods.
  • @DestinationVariable-annotated arguments for access to template variables extracted from the message destination. Values will be converted to the declared method argument type as necessary.
  • java.security.Principal method arguments reflecting the user logged in at the time of the WebSocket HTTP handshake.

@MessageMapping 메소드의 return value 는 org.springframework.messaging.converter.MessageConverter 를 통해 변환되고 새로운 메시지의 body 로 사용된다. 이 새로운 메시지는 기본적으로 “brokerChannel” 에 client 메시지와 같은 destination 이지만 기본값 ”/topic” prefix 를 사용하여 전달된다. @SendTo message level annotation 은 다른 destination 을 지정하고자 할 때 사용될 수 있다.

@SubscribeMapping annotation 은 @Controller 메소드에 구독 요청을 매핑하기 위해 사용될 수 있다. 이것은 method level 에 지원되지만, type level 의 @MessageMapping annotation 과 결합하여 같은 controller 내의 모든 message handling method 들과의 공유 매핑을 표현할 수 있다.

기본적으로 @SubscribeMapping 메소드의 return value 는 broker 에 전달되는 것이 아니라 바로 연결된 클라이언트들에 메시지로서 전달된다. 이것은 request-reply message 교환에 유용하다. 예를 들어, application UI 가 초기화 될 때 application data 를 가져오는 것이다. 대안으로서 @SubscribeMapping 메소드가 @SendTo 가 적용되는 경우 결과 message 는 지정된 대상 destination 을 사용하여 “brokerChannel” 에 전달된다.

5. Sending Messages

Application 의 어떤 부분에서라도 접속된 client 들에게 메시지를 보내고자 할 때 어떻게 해야 할까? 어떠한 application component 라도 “brokerChannel” 에 메시지를 전송할 수 있다. 이를 위한 가장 쉬운 방법은 SimpMessagingTemplate 을 주입받아서 메시지 전송에 사용하는 것이다. 이것은 다음의 예제를 통해 확인할 수 있다.

@Controller
public class GreetingController {
 
    private SimpMessagingTemplate template;
 
    @Autowired
    public GreetingController(SimpMessagingTemplate template) {
        this.template = template;
    }
 
    @RequestMapping(value="/greetings", method=POST)
    public void greet(String greeting) {
        String text = "[" + getTimestamp() + "]:" + greeting;
        this.template.convertAndSend("/topic/greetings", text);
    }
 
}

6. Simple Broker

기본 제공되는 단순한 message broker 는 클라이언트로부터의 구독 요청을 처리하고 그것을 메모리에 저장하고 일치하는 destination 을 가지는 연결된 client 들에 메시지를 brodcasting 한다. broker 는 Ant-style destination pattern 구독을 포함하는 동시에 path-like destination 을 지원한다.

Simple broker 가 초기 시작을 위해서는 훌륭하지만 STOMP 명령의 subset 들만을 지원하고 (e.g. no acks, receipts, etc) 단순 메시지 전송 loop 에 의존하며, clustering 에 적합하지 않다. 대신 application 은 full-featured message broker 를 사용하도록 업그레이드 할 수 있다.

적합한 message broker 선택 ( RabbitMQ, ActiveMQ, others) 을 위해 STOMP 문서를 확인하라. 그것을 설치하고 STOMP 지원을 활성화하여 broker 를 구동하라. 그 다음 Spring configuration 에서 simple broker 대신 STOMP broker relay 를 활성화 하라.

다음은 full-featured broker 를 활성화하는 설정예제이다.

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
 
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJS();
    }
 
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableStompBrokerRelay("/topic/", "/queue/");
        registry.setApplicationDestinationPrefixes("/app");
    }
 
}
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:websocket="http://www.springframework.org/schema/websocket" 
    xsi:schemaLocation=" 
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd">
 
    <websocket:message-broker application-destination-prefix="/app">
        <websocket:stomp-endpoint path="/portfolio" />
            <websocket:sockjs/>
        </websocket:stomp-endpoint>
        <websocket:stomp-broker-relay prefix="/topic,/queue" />
    </websocket:message-broker>
 
</beans>

위의 설정에서 “STOMP broker relay” 는 Spring MessageHandler 인데 이것은 외부 broker 에 메시지들을 forwarding 함으로써 메시지들을 처리한다. 그렇게 하기 위해 broker 에 TCP 연결을 맺고 모든 메시지를 broker 에 forwarding 하고 broker 로 부터 수신된 메시지를 WebSocket session 을 통해 client 에 역 forwarding (되돌려준다.) 한다. 본질적으로 그것은 마치 양방향으로 message 를 forwarding 하는 “relay” 처럼 동작한다.

org.projectreactor:reactor-net 의존성을 추가하여 TCP connection 관리를 할 수 있다.

게다가 application components (e.g. HTTP request handling method, business service, etc) 는 또한 메시지를 broker relay 에 전송할 수 있다. Section 20.4.5, “Sending Messages” 에서 subscribed WebSocket 클라이언트들에 메시지를 broadcast 하는 것이 설명되어 있다.

사실상 broker relay 는 강력하고 scalabale 한 message brodcasting 을 활성화 한다.

STOMP broker relay 는 broker 에 대한 단일 “system” TCP connection 을 유지한다. 이 connection 은 receiving message 가 아닌 server 측 application 에 기인하는 message 들만을 위해 사용된다. 이러한 접속을 위해 STOMP credentials 을 설정할 수 있다. (STOMP frame login 과 passcode 헤더) 이것은 Java config 와 XML 모두에서 systemLogin/systemPasscode 속성으로 노출된다. (기본값은 guest/guest 이다.)

STOMP broker relay 는 또한 모든 접속된 WebSocket client 를 위해 별도의 TCP 연결을 생성한다. 클라이언트를 대신해 모든 TCP 연결을 위해 STOMP credetial 을 설정할 수 있다. 이것은 Java config 와 XML 모두에서 clientLogin/clientPasscode 속성으로 노출된다. (기본값은 guest/guest 이다.)

STOMP broker relay 는 또한 “system” TCP connection 상에서 message broker 로부터의/로 hearbeat 을 전송하고 수신한다. heartbeat 전송/수신의 interval 을 설정할 수 있는데 기본값은 10 초이다. 만약 broker 에 대한 연결이 끊어지면 broker relay 는 재접속 시도를 성공할 때까지 5 초마다 계속한다.

Spring Bean 은 ApplicationListener<BrokerAvailabilityEvent> 를 구현할 수 있는데 이를 통해 broker 에 대한 “system” connection 이 끊기고 재접속될 때 알림을 받을 수 있다. 예를 들어, stock quote 를 broadcasting 하는 Stock Quote service 는 어떠한 active “system” connection 이 없을 때 메시지 전송시도를 중단할 수 있다.

STOMP broker relay 는 virtualHost 속성으로 설정될 수 있다. 이 속성의 값은 모든 CONNECT frame 의 host header 에 설정될 것이고 예를 들어 TCP connection 이 맺어진 실제 host 가 cloud-based STOMP service 를 제공하는 host 와 다른 cloud 환경에서 유용할 수 있다.

9. Authentication

WebSocket-style application 에서 누가 메시지를 보냈는지 아는것은 때때로 유용하다. 그래서 몇가지 인증 형태가 사용자 identity 를 형성하고 그것을 현재 session 에 연결하는데 필요하다.

Web application 은 이미 HTTP 기반의 인증을 사용한다. 예를 들어, Spring Security 는 보통 application 의 HTTP URL 을 보호할 수 있다. WebSocket session 이 HTTP handshake 로 시작하기 때문에 STOMP/WebSocket 에 매핑된 URL 은 이미 보호되고 인증을 요구한다는 것을 의미한다. 게다가 WebSocket 연결을 시작하는 page 는 그 스스로 거의 보호되는 것이고 실제 handshake 시까지 그 사용자는 인증되어져야 한다.

WebSocket hanshake 가 형성되고 새로운 WebSocket session 이 생성될 때 Spring WebSocket 지원은 자동적으로 HTTP request 로 부터 WebSocket session 으로 java.security.Principal 을 전달한다. 이후 WebSocket session 상의 application 을 통한 모든 message flow 는 사용자 정보가 포함되어진다. 그것은 message 의 header 에 존재한다. Controller 메소드는 javax.security.Principal 타입의 메소드 인자를 추가함으로 현재 사용자에 접근할 수 있다.

비록 STOMP CONNECT frame 이 인증을 위한 “login” 과 “passcode” 헤더를 가지고 있다하더라도, Spring 의 STOMP WebSocket 지원은 그것들을 무시하고 사용자가 HTTP 를 통해 기 인증되었기를 기대한다.

어떤 경우에는 사용자가 공식적으로 인증되지 않았을 때일지라도 WebSocket session 에 identity 를 부여하는것이 유용할 수도 있다. 예를 들면, mobile app 은 익명의 사용자에 어떤 identity (아마도 지리적 위치에 기반하여) 를 부여하기도 한다. 이때에 사용자 정의 handshake handler 가 사용될 수 있다. (Section 20.2.4, “Deployment Considerations” 에서 예제를 확인하라)

10. User Destinations

Application 은 특정 사용자를 지정하여 메시지를 전송할 수 있다. 연결된 사용자가 메시지를 수신하기 위해 그들은 인증되어져야 한다. 그래야 그들의 session 이 실제 user name 과 연결되기 때문이다. 앞선 section 에서 인증에 대해 확인하라

Spring 의 STOMP 지원은 /user/ 접두어로 destination 을 식별한다. 예를 들어, 클라이언트가 /user/position-updates destination 에 subscribe 할 수 있다. 이 destination 은 UserDestinationMessageHandler 에 의해 처리되고 사용자 session 에 unique 한 destination 으로 변환된다. (e.g. /user/position-updates-123) 이것은 일반적으로 명명된 destination 에 subscribing 편의를 제공하는 동시에 /user/position-updates 에 subscribe 하는 어떤 다른 사용자와 충돌하지 않음을 보장한다.

전송 측에서 메시지는 /user/{username}/position-updates 와 같은 destination 에 전송될 수 있다. 그러면 UserDestinationMessageHandler 에 의해 지정한 사용자 명에 속한 unique destination 과 같도록 번역될 것이다.

이것은 application 내의 어떤 component 도 name 과 일반적인 destination 만 알면 특정 사용자에 메시지를 전송할 수 있게 한다. 이것이 외부 message broker 와 같이 사용될 때 사용자 session 이 넘치면 모든 unique 한 사용자 queue 가 삭제되도록 하기 위해 inactive queue 를 다루는 방법에 대해 broker 문서를 확인하라. 예를 들어 RabbitMQ 는 /exchange/amq.direct/position-updates 와 같은 destination 이 사용될 때 auto-delete queue 를 생성한다. 클라이언트가 /user/exchange/amq.direct/position-updates 에 subscrbe 하는것이 그러한 경우이다. ActiveMQ 는 inactive destination 을 제거하기 위한 configuration options 이 있다.

11. ApplicationContext Events

STOMP messaging 지원은 다음의 ApplicationContext 이벤트를 발생시킨다. 이러한 이벤트들중 하나 혹은 그 이상을 처리하기 위해 Spring 관리 component 는 ApplicationListener 를 구현할 수 있다. 이벤트는 다음과 같다.

BrokerAvailabilityEvent — broker 가 available/inavailable 될 때 나타난다. “simple” broker 는 시작시 즉시 available 되고 application 구동중 유지되지만 STOMP “broker relay” 는 만약 broker 가 재시작되면 full featured broker 에 대한 연결을 잃을 지도 모른다. broker relay 는 reconnect logic 을 가지고 있고 broker 에 대한 “system” connection 을 broker 가 돌아오면 재설정 할 것이다. 그 결과 이 event 는 상태가 connected 에서 disconnected 가 될 때나 그 반대일때마다 발생한다. SimpMessagingTemplate 을 사용하는 Components 는 이 이벤트에 subscribe 하여야 하며 broker 가 이용불가일 때 메시지를 전송하지 말아야 한다. 어떤 경우에서는 메시지를 전송할 때 MessageDeliveryException 을 처리하도록 준비되어야 한다. SessionConnectEvent — 새로운 클라이언트 session 을 나타내는 STOMP CONNECT 가 수신될 때 나타난다. 이 이벤트는 session id , user information 및 client 가 보냈을 지 모르는 어떤 사용자 정의 header 를 포함한다. 이것은 client sessions 을 추적하는데 유용하다. 이 이벤트에 subscribe 하는 Components 는 SimpMessageHeaderAccessor 나 StompMessageHeaderAccessor 를 사용하여 메시지를 wrapping 할 수 있다. SessionConnectedEvent — broker 가 SessionConnectEvent 이후 CONNECT 에 대한 응답으로 STOMP CONNECTED frame 을 전송했을 때 짧게 발생한다. 이 시점에 STOMP session 은 완전히 연결되었음으로 간주될 수 있다. SessionDisconnectEvent — STOMP session 이 끝났을 때 발생한다. DISCONNECT 는 client 로 부터 전송되거나 WebSocket session 이 closed 될 때 자동적으로 발생되어질 수 있다. 어떤 경우에 이 이벤트는 세션당 한번 이상 발생할수도 있다. Components 는 복수의 disconnect events 에 대해 idempotent(멱등) 해야 한다. full-featured broker 를 사용할 때, STOMP “broker relay” 는 자동적으로 broker 가 일시적으로 이용불가한 “system” connection 을 재접속 한다. 그러나 Client connections 는 자동적으로 재접속 되지 않는다. heartbeats 이 활성화 되어 있다면, client 는 일반적으로 10초 안에 broker 가 응답하지 않음을 알게 될 것이다. Clients 는 스스로 reconnect logic 을 구현해야할 필요가 있다.

12. Configuration and Performance

성능에 대한 은총알은 없다. 많은 요인 (메시지 size, volume, application 메소드가 blocking 을 요하는 작업을 수행하는지 여부, network 속도 같은 외부요소 및 기타등등) 이 영향을 끼친다. 이 section 의 목적은 scaling 에 대한 생각과 더불어 이용가능한 configuration options 의 개요를 제공하는 것이다.

messaging application 에선 messages 는 thread pool 에 기반한 비동기 수행을 위해 channels 을 통해 전달된다. 이런 application 을 설정하는 것은 channels 과 messages flow 에 대한 지식이 요구된다. 그래서 Section 20.4.3, “Flow of Messages” 를 리뷰해보기 바란다.

시작하기 좋은 위치는 “clientInboundChannel” 과 “clientOutboundChannel” 를 지지하는 thread pools 을 설정하는 것이다. 기본적으로 이 둘은 사용가능한 processor 수의 2배로 설정된다.

막약 annotated method 에서 메시지 처리가 주로 CPU 소비에 기인하면 “clientInboundChannel” 의 thread 수는 processor 수에 가깝게 유지되어야 한다. 만약 작업이 IO 소비에 기인하고 database 나 다른 external system 에 blocking 을 요한다면 thread pool size 는 증가되어야 할 필요가 있다.

ThreadPoolExecutor 는 3 가지 중요한 property 를 갖는다. 그것은 core 와 max thread pool size 와 이용가능한 thread 가 없을 때 작업을 저장하기 위한 queue capacity 이다.

일반적인 혼동이 오는 지점은 core pool size (e.g. 10) 와 max pool size (e.g. 20) 설정이 10 에서 20 threads 를 가지는 thread pool 로 나타나는 것이다. 사실 capacity 가 그 기본값인 Integer.MAX_VALUE 인채로 남겨두면 thread pool 은 모든 추가적인 작업이 queue 에 쌓일 것이기 때문에 결코 core pool size 위로 증가하지 않을 것이다.

이러한 properties 들이 어떻게 작용하고 다양한 queuing strategies 를 이해하기 위해 ThreadPoolExecutor 의 Javadoc 을 확인하라.

“clientOutboundChannel” 측에서 thread pool 은 온전히 WebSocket clients 에 메시지를 전송하는 것과 관련있다. 만약 clients 가 빠른 network 에 있다면 스레드 수는 이용가능한 processors 수에 가깝게 유지되어야 한다. 만약 그들이 느리거나 low bandwidth 상에 있다면 그들은 메시지 소비를 위해 더 오래 점유할 것이고 thread pool 에 부담을 줄 것이다. 그래서 thread pool size 증가가 필요할 수도 있을 것이다.

“clientInboundChannel” 에 대한 부하는 예상가능하다 — 결국 그것은 application 이 무엇을 하는지에 기인한다.— ”clientOutboundChannel” 을 어떻게 설정해야 하는지는 application 제어를 넘어서는 요인에 기인하기 때문에 좀더 어렵다. 이러한 이유로 메시지 전송과 관련한 두가지 추가적인 properties 가 있다. 그것은 “sendTimeLimit” 와 “sendBufferSizeLimit” 이다. 이것은 얼마나 오래 그 전송이 허락되는가와 얼마나 많은 데이터가 client 에 메시지 전송시 buffer 되는지를 설정하는데 사용된다.

일반적인 생각은 어떤 주어진 시간에 단 하나의 thread 만이 client 에 메시지 전송을 위해 사용되어져야 한다는 것이다. 모든 추가적인 메시지는 동시에 버퍼링 되고 이러한 properties 가 얼마나 오래 메시지 전송에 점유가 허락되는가와 얼마나 많은 데이터가 동시에 버퍼링 되는가에 사용할 수 있다. 중요한 추가적인 세부사항은 XML schema 의 Javadoc 을 참고하라.

다음은 설정 예이다.

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
 
    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024);
    }
 
    // ...
 
}
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:websocket="http://www.springframework.org/schema/websocket" 
    xsi:schemaLocation=" 
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd">
 
    <websocket:message-broker>
        <websocket:transport send-timeout="15000" send-buffer-size="524288" />
        <!-- ... -->
    </websocket:message-broker>
 
</beans>

위의 WebSocket 전송 설정은 들어오는 STOMP messages 에 허락되는 최대 사이즈를 설정하는데 사용될 수 있다. 비록 이론적으로 WebSocket message 가 size 에 제한이 없지만, 실제 WebSocket servers 는 한계를 부여한다. 예를 들어 Tomcat 에서 8K 이고 Jetty 에서 64K 이다. 이런 이유로 stomp.js 같은 STOMP clients 는 커다란 STOMP messages 를 16K boundaries 로 분리하고 그들을 복수개의 WebSocket messages 로 전송한 후 서버가 buffer 해서 재조합하도록 한다.

Spring 의 STOMP over WebSocket 지원은 이것을 수행한다. 그래서 applications 은 STOMP messages 를 위해 WebSocket server 고유의 message sizes 를 무시한 채 maximum size 를 설정할 수 있다. WebSocket message size 가 만약 최소 16K WebSocket messages 전송을 보장하여야 한다면 자동적으로 조정되어질 것을 명심하라.

다음은 설정예시이다.

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
 
    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration.setMessageSizeLimit(128 * 1024);
    }
 
    // ...
 
}
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:websocket="http://www.springframework.org/schema/websocket" 
    xsi:schemaLocation=" 
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd">
 
    <websocket:message-broker>
        <websocket:transport message-size="131072" />
        <!-- ... -->
    </websocket:message-broker>
 
</beans>

scaling 에 대한 중요한 point 는 복수개의 application instances 를 사용하는 것이다. 현재 simple broker 로는 불가능하다. 그러나 RabbitMQ 같은 full-featured broker 를 사용할 때, 각 application instance 는 broker 에 연결하고 하나의 application instance 로부터의 messages broadcast 는 어떤 다른 application instances 를 통해 연결된 WebSocket clients 에 그 broker 를 통해 broadcast 할 수 있다.

13. Testing Annotated Controller Methods

Spring 의 STOMP over WebSocket 지원에 2가지 주요한 접근이 있다. 첫번째는 controller 의 기능과 annotated message handling method 의 기능을 검증하는 server-side tests 를 작성하는 것이다. 두번째는 client 와 server 구동과 관련한 전체 end-to-end tests 를 작성하는 것이다.

2 가지 접근은 상호 보완적이지 않다. 그와는 반대로 전체 test 전략에 각각 위치한다. Server-side tests 는 좀더 집중되고 작성하고 유지하기가 쉽다. 반면에 End-to-end integration tests 는 좀 더 완벽하고 테스트가 더 많지만 그것들은 작성과 유지에 더 많이 관련되어 진다.

가장 단순한 server-side tests 는 controller unit tests 를 작성하는 것이다. 그러나 이것은 충분히 유용하지 않다. 왜냐하면 controller 의 많은 부분이 annotations 에 의존하기 때문이다. 순수한 unit tests 는 그것을 테스트하지 못한다.

이상적으로 테스트 하의 controllers 는 마치 SpringMVC Test framework 에서 HTTP 요청을 처리하는 controller 를 테스트하는 접근같이 runtime 에서 처럼 호출되어야 한다. 즉 구동중인 Servlet container 없이 Spring Framework 가 annotated controllers 를 호출하는 것이다. Spring MVC Test 와 같이 2가지 가능한 대안이 있다. 그것은 “context-based” 나 “standalone” setup 을 사용하는 것이다.

Spring TestContext framework 의 도움으로 실제 Spring configuration 로드하고 “clientInboundChannel” 를 테스트 field 로 주입받아서, 이를 사용하여 메시지를 전송한다. 수동적으로 controllers 를 호출하기 위해 필요한 최소한의 Spring framework infrastructure 를 설정한다. (SimpAnnotationMethodMessageHandler) 그리고 메시지를 controller 에 직접 전달한다. 이 두가지 시나리오는 tests for the stock portfolio sample application 에서 시연되고 있다.

두번째 접근은 end-to-end integration tests 를 만드는 것이다. 이를 위해 WebSocket server 를 embedded mode 로 구동할 필요가 있고 STOMP frame 을 포함하는 메시지를 전송하는 WebSocket client 로서 서버에 접속한다. stock portfolio sample application 에 대한 test 는 embedded WebSocket 서버로서 Tomcat 을 사용하고 test 목적의 간단한 STOMP client 를 사용하는 접근을 보여준다.

3.28 - SockJS Fallback Options

Spring은 WebSocket이 지원되지 않는 환경에서 SockJS 프로토콜을 사용해 WebSocket API를 에뮬레이션하는 fallback 옵션을 제공한다.

SockJS Fallback Options

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-fallback

WebSocket 이 아직까지 모든 브라우저에서 지원되지 않거나 네트워크 프락시 제약등으로 사용할 수 없는 경우가 있다. 이에 Spring 은 fallback 옵션을 제공하는데 이는 SockJS protocol 에 기반으로 WebSocket API 를 emulate 한다.

1. SockJS 개요

SockJS 는 application 으로 하여금 WebSocket API 를 사용하는데 있다. 만약 WebSocket 사용이 불가한 경우에도 이를 fallback option 으로 제공하여 어떠한 코드 변화없이 WebSocket API 를 사용토록 한다.

SockJS 구성

SockJS 는 여러가지 테크닉을 이용하여 다양한 브라우저 및 브라우저 버전을 지원한다. 전송 타입은 다음의 3가지로 분류된다. WebSocket, HTTP Streaming, HTTP Long Polling. 이들 각각을 살펴보려면 여기 를 참조한다.

SockJS client 는 서버의 기본 정보를 얻기 위해서 “GET /info” 를 호출한다. 그 이후에 SockJS 는 어떤 전송 타입을 사용할지를 결정한다. WebSocket 은 최우선책이며, 이후로 HTTP Streaming > HTTP (long) polling 이 사용된다.

모든 전송 요청은 다음의 URL 구조를 갖는다.

http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}

  • {server-id} - 클러스터에서 요청을 routing 하는데 사용하나 이외에는 의미가 없다.
  • {session-id} - SockJS session 에 소속하는 HTTP 요청과 연관성이 있다.
  • {transport} - 전송타입 ex> websocket, xhr-streaming, 기타 등등

WebSocket 전송은 WebSocket handshaking 을 위한 오직 하나의 HTTP 요청을 필요로 한다. 모든 메시지들은 그 이후에 사용했던 socket 을 통해 교환된다.

HTTP 전송은 좀 더 많은 요청을 필요로 한다. 예를 들어 Ajax/XHR streaming 은 server 에서 client 로의 메시지를 위해 하나의 long-running 요청이 있고 추가적인 HTTP POST 요청은 client 에서 server 로의 메시지를 위해 사용된다. Long polling 은 server 가 client 로의 응답 후에 현재의 요청을 끝내는 것을 제외하고는 XHR streaming 과 유사하다.

SockJS 는 최소한의 message framing 을 추가한다. 예를 들어 server 는 “o” (open frame) 를 초기에 전송하고, 메시지는 [“message1”,”message2”] 와 같은 JSON-encoded 배열로서 전달되며, 문자 “h” (hearbeat frame) 는 기본적으로 25초간 메시지 흐름이 없는 경우에 전송하고 “c” (close frame) 는 해당 세션을 종료한다.

이에 대한 자세한 이해는 브라우저에서 확인할 수 있다. SockJS 는 debug flag 를 제공하고, 전송타입을 고정하여 각각에 대해서 살펴볼 수 있다. 서버는 “org.springframework.web.socket” 에 TRACE 로깅을 활성화 하여 로그를 볼 수 있다.

2. SockJS 활성화

SockJS 는 설정을 통해 쉽게 활성화 할 수 있다.

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
 
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/myHandler").withSockJS();
    }
 
    @Bean
    public WebSocketHandler myHandler() {
        return new MyHandler();
    }
 
}
<beans xmlns="http://www.springframework.org/schema/beans" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:websocket="http://www.springframework.org/schema/websocket" 
    xsi:schemaLocation=" 
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        http://www.springframework.org/schema/websocket/spring-websocket-4.0.xsd">
 
    <websocket:handlers>
        <websocket:mapping path="/myHandler" handler="myHandler"/>
        <websocket:sockjs/>
    </websocket:handlers>
 
    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>
 
</beans>

위의 설정은 Spring MVC 에서 사용하기 위한 것으로 DispatcherServlet 설정에 포함되어져야 한다. (application context 를 계층적으로 가져갈 경우) 그러나 Spring WebSocket 이 SpringMVC 이외에도 사용할 수 있도록 제공되듯, SockJS 도 그러하다. 이는 SockJsHttpRequestHandler 를 통해서 제공된다.

browser (클라이언트) 측에서는 W3C WebSocket API 를 emulate 하는 sockjs-client 를 사용할 수 있다. 이를 통해 서버와 통신하여 최적의 전송타입을 선택한다. 이 외에도 포함할 전송타입을 지정하는 몇가지 설정을 제공한다.

3. IE 8, 9 에서의 HTTP Streaming (Ajax/XHR vs IFrame)

IE 8/9 는 대중적으로 사용되고 있다. 이것은 SockJS 가 필요한 핵심적인 이유이다.

SockJS 는 Ajax/XHR Streaming 을 Microsoft 의 XDomainRequest 를 통해 지원하고 있다. 이것은 서로 다른 도메인 간에도 동작하지만 cookie 전송을 지원하지 않는다. Cookie 는 Java Application 에서 매우 필수적이다. 그러나 SockJS 클라이언트는 Java 만을 위한 것이 아닌 많은 서버 타입을 위해 사용되도록 고안되었기 때문에 Cookie 를 중요하게 다룰지 여부를 알려주어야 한다. SockJS 클라이언트는 Ajax/XHR Streaming 을 선택하거나 그렇지 않다면 iframe 기반의 technique 을 사용한다.

SockJS 클라이언트로 부터의 최초 요청인 ‘/info’ 는 클라이언트의 전송 타입 선택을 위한 정보를 위한 요청이다. 상세한 내용 중 하나는 서버 application 이 cookie 에 의존 (authentication 목적이나 session clustering with stick mode 등) 하는지 여부이다. Spring 의 SockJS 지원은 sessionCookieNeeded 라는 속성을 포함한다. 이것은 Java application 이 JSESSIONID cookie 에 의존하기 때문에 기본적으로 활성화 된다. 만약 이 기능을 OFF 한다면 비로서 SockJS 클라이언트는 xdr-streaming 을 IE 8/9 에서 사용토록 선택되어 진다.

iframe 기반의 전송을 사용한다면 browser 가 HTTP 응답헤더인 X-Frame-Option 이 DENY, SAMEORIGIN, ALLOW-FROM <origin> 로 설정됨에 따라 iframe 사용을 블락시킬수 있음을 염두에 두어야 한다. 이러한 헤더는 clickjacking 이라 알려진 공격을 방어하기 위한 목적으로 주로 사용된다.

Spring Security 3.2+ 에서 X-Frame-Options 헤더를 모든 응답에 설정하도록 지원하고 있다. 즉 Spring Security Java Config 에서 이 값을 DENY 로 기본설정한다. Spring Security XML 설정에서는 이 헤더를 설정하지 않지만 차후에 기본값으로 설정될 여지가 있다.

Spring Security 의 Section 7.1. Default Security Headers 에서 X-Frame-Options 헤더 설정에 대한 상세 문서를 살펴볼 수 있다. 이에 대한 추가적인 배경을 보고자 한다면 SEC-2501 을 살펴본다.

X-Frame-Option 응답헤더를 추가한다면 (Spring Security 를 사용한다면 그렇게 된다.) 헤더 값을 SAMEORIGIN 이나 ALLOW-FROM <origin> 으로 설정할 필요가 있다. 이와 더불어 Spring SockJS 지원은 SockJS 클라이언트의 location 을 알아야 할 필요가 있다. 왜냐하면 client 가 iframe 상에서 로드될 것이기 때문이다. 기본적으로 iframe 은 SockJS 클라이언트 (sockJS.js) 를 CDN location 으로부터 다운되도록 설정되어 있다. 이를 same origin 으로 설정하는 것이 필요하다.

Java Config 에서 아래와 같이 설정할 수 있다. XML 에서는 <websocket:sockjs> 에서 설정한다.

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
 
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJS()
                .setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js");
    }
 
    // ...
 
}

초기 개발중에는 SockJS client devel mode 를 활성화 하라. 이는 SockJS request (iframe 같은) 를 브라우저가 캐시하는 것을 방지한다. 이에 대한 방법은 SockJS client page 에서 확인할 수 있다.

4. Heartbeat Messages

SockJS 프로토콜은 server 가 hearbeat message 를 전송하도록 하여 proxies 가 connection 을 hung 으로 인식하지 못하도록 할 필요가 있다. Spring SockJS 설정은 heartbeatTime 속성을 가지는데 빈도를 조정하는데 사용된다. 기본값은 해당 커넥션에 어떤 메시지도 없는 25초를 사용한다. 이 25초는 IETF 권고안 을 따르고 있다.

STOMP over WebSocket/SockJS 를 사용할 때 만약 STOMP 클라이언트와 서버가 heartbeat 교환에 합의한다면 SockJS heartbeat 은 비활성화 된다.

Spring SockJS 지원은 heartbeat 작업을 스케줄링하기 위해 TaskScheduler 를 설정하도록 하고 있다. TaskScheduler 는 이용가능한 processor 의 수에 따른 기본값으로 설정된 thread pool 로부터 가져오는데 이는 특정한 요구에 적합한 값을 설정하는 것을 고려해야 한다.

5. Servlet 3 Async Requests

HTTP streaming 과 HTTP long polling SockJS 전송은 커넥션을 보통의 경우보다 오래 open 되도록 한다. 이것에 대한 개요는 다음 에서 확인한다.

Servlet Container 에서 이것은 Servlet 3 async 지원을 통해 수행되는데 이것은 요청을 처리중인 Servlet 스레드를 빠져나오고 다른 Servlet 스레드에서 응답을 기록하도록 한다.

Servlet API 가 가지는 특정 이슈가 있는데 그것은 클라이언트의 갑작스러운 사라짐에 어떠한 알림도 제공하지 않는 것이다. ( SERVLET_SPEC-44 참고) 그러나 Servlet container 는 응답을 write 하기 위한 이어지는 시도에 예외를 발생한다. Spring SockJS 지원은 기본 25초 간격의 heartbeat message 를 전송하므로 이를 통해 보통 client disconnect 는 주기 안에 발견되어 진다. (주기를 짧게 하면 더 빨리 알게됨)

결론적으로, Network IO failure 은 client disconnect 에 대해서 빈번하게 발생할 여지가 있다. 이것은 로그를 stack trace 로 채우게 될 수도 있다. Spring 은 이러한 client disconnect 를 식별하기위해 최선의 노력을 하고 이에 따른 최소한의 메시지를 기록하려고 노력한다. (이 로그는 AbstractSockJsSession 에 정의된 DISCONNECTED_CLIENT_LOG_CATEGORY 로그 카테고리를 사용한다. 만약 stack trace 를 보고자 한다면 해당 로그 카테고리를 TRACE 로 설정하라)

6. CORS Headers for SockJS

SockJS protocol 은 XHR streaming 과 polling 전송에 cross-domain 지원을 위한 CORS 헤더를 사용한다. 그래서 CORS 헤더가 응답에서 발견되지 않는다면 CORS 헤더들이 자동으로 추가된다. 만약 Servlet Filter 등을 통해서 CORS 헤더가 이미 설정된다면 Spring SockJsService 는 이를 스킵한다.

다음은 SockJS 에 의해 기대되는 header 와 값들이다.

  • “Access-Control-Allow-Origin” - intitialized from the value of the “origin” request header or “*”.
  • “Access-Control-Allow-Credentials” - always set to true.
  • “Access-Control-Request-Headers” - initialized from values from the equivalent request header.
  • “Access-Control-Allow-Methods” - the HTTP methods a transport supports (see TransportType enum).
  • “Access-Control-Max-Age” - set to 31536000 (1 year).

정확한 구현을 보고자 한다면 AbstractSockJsService.addCorsHeaders() 와 TransportType enum 을 소스에서 살펴보라.

대안으로서 CORS 설정은 SockJS endpoint prefix 를 제외 URL 목록으로 고려한다면, Spring SockJsService 가 그것을 처리토록 한다.

3.29 - UI - bootstrap

Bootstrap은 반응형 웹 디자인을 쉽게 구현하기 위한 프론트엔드 프레임워크로, 다양한 CSS, JavaScript 컴포넌트와 UI 템플릿을 제공하여 빠르게 웹사이트를 구축할 수 있다. 또한 그리드 시스템, 다양한 UI 컴포넌트, 모달과 같은 동적 기능들을 지원하며, 모바일 중심으로 설계되어 다양한 디바이스에서 최적화된 화면을 제공한다.

UI - bootstrap

개요

부트스트랩(Bootstrap)은 웹디자인을 쉽게 하기 위해 트위터에서 오픈 소스로 공개한 프런트 엔드 프레임워크로, 유연한 HTML, CSS, JavaScript 템플릿과 UI컴포넌트, 인터렉션을 제공하여 손 쉽게 웹 사이트를 구축할 수 있는 시작점이 된다.

부트스트랩의 장점은 크게 다음과 같다.

  • 부트스트랩 3 이후부터 모바일 중심의 프레임워크이다
  • 다양한 브라우저들을 지원한다.
  • 반응형 웹에 최적화 되어있다. 부트스트랩은 스마트폰, 태블릿, 데스크탑에 최적화 되어 css가 조정이 된다.
  • 시작이 용이하다. HTML, CSS만 알고 있어도 부트스트랩의 사용이 가능하다.

이러한 장점들로 인해 표준프레임워크에서는 실행환경 UI로 bootstrap을 선정하였다.

본 가이드에서는 부트스트랩의 기본적인 소개와 몇 가지의 예제를 제공한다.

자세한 내용은 부트스트랩 사이트의 가이드나 w3schools의 튜토리얼을 참조하도록 한다.

시작하기

부트스트랩을 사용하기 위해서는 부트스트랩 관련 CSS와 JavaScript를 추가해 주어야 한다. 추가하는 방법은 두가지로 부트스트랩을 다운로드 하여 사용하거나, 다운로드 하지 않을 경우 CDN 링크를 추가하여 사용 한다.

※ 주의사항 : 부트스트랩을 사용하려면 jQuery가 필요하므로 반드시 별도로 추가하여 주어야 한다.

  • 다운로드
    • 부트스트랩 사이트의 메뉴 중 시작하기 > 다운로드 항목에서 원하는 종류의 다운로드를 진행한다.
    • 부트스트랩 사이트에서는 여러가지 다운로드를 제공하는데, 본 가이드에서는 프리컴파일 된 부트스트랩을 다운로드하여 진행하였다.
      • 프리컴파일 된 부트스트랩 : 컴파일되고 최소화된 CSS, 자바스크립트, 폰트 제공. 문서나 원본 파일들은 포함되어 있지 않음
  • 부트스트랩 CDN
    • MaxCDN에서 부트스트랩의 CSS 와 자바스크립트를 CDN으로 지원한다. 이를 사용하려면, 아래의 부트스트랩 CDN 링크들을 추가하면 사용할 수 있다. 자세한 내용은 다음 링크(download-cdn)를 확인 한다.
<!-- 최신 컴파일 및 최소화된 최신 CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" 
        integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">

<!-- 옵션 테마 -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css" 
        integrity="sha384-fLW2N01lMqjakBkx3l/M9EahuwpSfeNvV63J5ezn3uZzapT0u7EYsXMjQV+0En5r" crossorigin="anonymous">


<!-- jQuery library 부트스트랩을 사용하려면 jQuery가 필요함-->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>


<!-- 최신 컴파일 및 최소화된 자바스크립트 -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" 
        integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>

파일 구조

프리 컴파일 된 부트스트랩을 다운로드하고, 압축을 해제하면 다음과 같은 구조를 볼 수 있다.

bootstrap/
├── css/
│   ├── bootstrap.css
│   ├── bootstrap.css.map
│   ├── bootstrap.min.css
│   ├── bootstrap-theme.css
│   ├── bootstrap-theme.css.map
│   └── bootstrap-theme.min.css
├── js/
│   ├── bootstrap.js
│   └── bootstrap.min.js
└── fonts/
    ├── glyphicons-halflings-regular.eot
    ├── glyphicons-halflings-regular.svg
    ├── glyphicons-halflings-regular.ttf
    ├── glyphicons-halflings-regular.woff
    └── glyphicons-halflings-regular.woff2

위의 구조는 어느 웹프로젝트에도 쉽게 적용하기 위한 가장 기본적인 형태다. 컴파일 된 CSS와 JavaScript(bootstrap.)를 제공하고, 컴파일 되고 최소화된 CSS와 JavaScript(bootstrap.min.) 도 제공한다.

그리고 Glyphicons 의 폰트 파일들과 부가적인 부트스트랩 테마 파일들도 같이 존재한다.

부트스트랩의 특징

HTML5

부트스트랩은 HTML5의 HTML요소와 CSS속성을 요구하기 때문에 HTML5 doctype을 사용해야 한다. 항상 페이지의 시작부분에 HTML5 doctype<head>내에 lang 속성과 character set을 추가해 주어야 한다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="utf-8"> 
  </head>
</html>

모바일 친화적

부트스트랩2 에서는 모바일 친화적인 스타일을 프레임워크의 core로 추가했는데, 부트스트랩3 부터는 시작부터 모바일 친화적으로 설계 되었다.

다음은 페이지의 렌더링과 확대/축소를 사용하기 위한 것으로, <head> 내에 viewport 메타 태그를 추가한다.

<meta name="viewport" content="width=device-width, initial-scale=1">
  • width=device-width
    • device의 화면 폭(width)에 따라 페이지의 화면 폭을 설정한다.(device마다 달라짐)
  • initial-scale=1
    • 페이지가 처음 브라우저에 로드 될 때 초기 zoom level을 설정한다.
  • user-scalable=no
    • 모바일 기기의 Zoom기능을 끌 수 있다.

컨테이너

부트스트랩은 사이트 전체의 콘텐츠를 감싸는 컨테이너 요소가 필요하다.

다음의 두 예제는 컨테이너에 대한 예제이다.

※ jsfiddle.net을 이용하여 아래의 예제코드를 실행 결과를 링크로 제공.

예제 1 : container

<!DOCTYPE html>
<html lang="ko">
   <head>
      <title>Bootstrap Example</title>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" 
         integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
      <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" 
         integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
   </head>
   <body>
      <div class="container">
         <h1>My First Bootstrap Page</h1>
         <p>This is some text.</p>
      </div>
   </body>
</html>

결과 보기 : https://jsfiddle.net/ymxvbzo7/?utm_source=website&utm_medium=embed&utm_campaign=ymxvbzo7

예제 2 : container-fluid

<!DOCTYPE html>
<html lang="ko">
   <head>
      <title>Bootstrap Example</title>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" 
         integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
      <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" 
         integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
   </head>
   <body>
      <div class="container-fluid">
         <h1>My First Bootstrap Page</h1>
         <p>This is some text.</p>
      </div>
   </body>
</html>

결과 보기 : https://jsfiddle.net/y1hL9vqd/1/?utm_source=website&utm_medium=embed&utm_campaign=y1hL9vqd

bootstrap-myfirstbootstrap-sample1

bootstrap-myfirstbootstrap-sample2

위의 그림처럼 두 예제의 결과보기의 화면의 폭을 좌우로 늘려보면, 두 예제의 차이점을 알 수 있다.

첫번째 예제는 고정 폭 컨테이너(<div class="container">)를 사용하였고, 두번째 예제는 최대 폭 컨테이너(<div class="container-fluid">)를 사용한 예제이다.

그 밖에 세부 내용은 링크를 참조하도록 한다.

그리드 시스템

부트스트랩은 반응형, 페이지 레이아웃을 위해 자체적인 그리드 레이아웃 시스템을 제공한다.

부트스트랩의 그리드 시스템은 12열의 그리드(12-column grid)로 구성되어 있으며, device나 viewport의 크기에 따라 자동으로 열이 적절한 크기로 배열되게 한다.

고려사항

  • 행(row)은 반드시 .container(고정폭) 나 .container-fluid(전체폭) 안에 위치해야 한다.
  • 열(column)들은 수평그룹을 만드는데 행을 이용한다.
  • 콘텐츠는 열 안에 위치해야 한다. 그리고 열 들만이 행의 바로 아래에 올 수 있다.
  • .row.col-xs-4 같은 사전 정의된 그리드 클래스들을 통해 간편하게 그리드 레이아웃 만들 수 있다.
  • 열은 padding 으로 여백을 줄 수 있다. 패딩은 행 내에서 첫 열과 마지막 열을 위해 .row 내에 음수 마진으로 offset 되어 있다.
  • 그리드 열은 12개의 가능한 열들을 원하는 만큼 명시하는 것으로 만들어진다.( ex) 같은 크기의 3개 열 : .col-xs-4 를 3개 사용)

기타사항은 링크 내용을 참조하도록 한다.

그리드 옵션

매우 작은 기기 모바일 폰( < 768px)작은 기기 태블릿 (≥768px)중간 기기 데스크탑 (≥992px)큰 기기 데스크탑 (≥1200px)
그리드 적용항상분기점보다 크면 적용
컨테이너 너비없음 (auto)750px970px1170px
클래스 접두사.col-xs-.col-sm-.col-md-.col-lg-
컬럼 수12
컬럼 너비Auto~62px~81px~97px
사이 너비30px (컬럼의 양쪽에 15px 씩)

세부내용은 링크의 표를 참조한다.

기본적인 부트스트랩 그리드 구조

<div class="row">
  <div class="col-sm-4">.col-sm-4</div>
  <div class="col-sm-4">.col-sm-4</div>
  <div class="col-sm-4">.col-sm-4</div>
</div>
  • 행(row)를 생성해야 한다.(<div class="row">)
  • 행 안에 적절한 수의 열(column)을 추가한다. (<div class="col-sm-4">.col-sm-4</div>)
  • 행 안의 열의 수는 항상 12열을 맞춰야 한다.

그리드 예제 : Stacked-to-horizontal

<!DOCTYPE html>
<html lang="ko">
   <head>
      <title>Bootstrap Example</title>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" 
         integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
      <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" 
         integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
   </head>
   <body>
      <div class="container">
         <h1>Grid</h1>
         <p>This example demonstrates a 50%/50% split on small, medium and large devices. On extra small devices, it will stack (100% width).</p>
         <p>Resize the browser window to see the effect.</p>
         <div class="row">
            <div class="col-sm-6" style="background-color:yellow;">
               Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.<br>
               Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
            </div>
            <div class="col-sm-6" style="background-color:pink;">
               Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto.    
            </div>
         </div>
      </div>
   </body>
</html>

bootstrap-grid-sample

결과 보기 : https://jsfiddle.net/eh5kmtt9/1/?utm_source=website&utm_medium=embed&utm_campaign=eh5kmtt9

결과창의 좌우폭을 확대 및 축소를 해보면, 화면 사이즈에 맞춰서 열(column)이 쌓이거나(stack) 수평이 되게 늘어나는 것을 확인 할 수 있다.이와 같이 손 쉽게 반응형을 지원하고, 페이지 레이아웃을 구성할 수 있다.

그 밖의 예제들은 부트스트랩 사이트 그리드 부분에서 내용을 확인하도록 한다.

다양한 스타일의 CSS

부트스트랩은 깔끔하고 잘 정돈된 느낌의 다양한 스타일의 CSS를 제공한다. 자세한 내용은 링크의 내용을 확인하도록 한다.

테이블

다음은 부트스트랩에서 제공하는 클래스를 적용한 테이블 예제이다.

<!DOCTYPE html>
<html lang="ko">
   <head>
      <title>Bootstrap Example</title>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" 
         integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
      <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" 
         integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
   </head>
   <body>
      <div class="container">
         <h2>Contextual Classes</h2>
         <p>Contextual classes can be used to color table rows or table cells. The classes that can be used are: .active, .success, .info, .warning, and .danger.</p>
         <table class="table">
            <thead>
               <tr>
                  <th>Firstname</th>
                  <th>Lastname</th>
                  <th>Email</th>
               </tr>
            </thead>
            <tbody>
               <tr class="success">
                  <td>John</td>
                  <td>Doe</td>
                  <td>example1@example.com</td>
               </tr>
               <tr class="danger">
                  <td>Mary</td>
                  <td>Moe</td>
                  <td>example2@example.com</td>
               </tr>
               <tr class="info">
                  <td>July</td>
                  <td>Dooley</td>
                  <td>example3@example.com</td>
               </tr>
            </tbody>
         </table>
      </div>
   </body>
</html>

결과보기 : https://jsfiddle.net/r60oymr2/3/?utm_source=website&utm_medium=embed&utm_campaign=r60oymr2

bootstrap-table-sample

부트스트랩에서 기본적으로 제공하는 테이블 디자인 중 하나이다.

다음은 부트스트랩에서 제공하는 폼의 예제이다.

<!DOCTYPE html>
<html lang="ko">
   <head>
      <title>Bootstrap Example</title>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" 
         integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
      <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" 
         integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
   </head>
   <body>
      <div class="container">
         <h2>Inline form</h2>
         <p>Make the viewport larger than 768px wide to see that all of the form elements are inline, left aligned, and the labels are alongside.</p>
         <form class="form-inline" role="form">
            <div class="form-group">
               <label for="email">Email:</label>
               <input type="email" class="form-control" id="email" placeholder="Enter email">
            </div>
            <div class="form-group">
               <label for="pwd">Password:</label>
               <input type="password" class="form-control" id="pwd" placeholder="Enter password">
            </div>
            <div class="checkbox">
               <label><input type="checkbox"> Remember me</label>
            </div>
            <button type="submit" class="btn btn-default">Submit</button>
         </form>
      </div>
   </body>
</html>

결과보기 : https://jsfiddle.net/4006oqs0/1/?utm_source=website&utm_medium=embed&utm_campaign=4006oqs0

bootstrap-form-sample

위의 예제는 인라인 폼(inline form)이 적용 된 예제로, 화면의 폭에 따라 입력창의 위치가 변화한다.

버튼

다음은 부트스트랩에서 제공하는 버튼 디자인이다.

<!DOCTYPE html>
<html lang="ko">
   <head>
      <title>Bootstrap Example</title>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" 
         integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
      <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" 
         integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
   </head>
   <body>
      <div class="container">
         <h2>Button</h2>
         <!-- Standard button -->
         <button type="button" class="btn btn-default">Default</button>
         <!-- Provides extra visual weight and identifies the primary action in a set of buttons -->
         <button type="button" class="btn btn-primary">Primary</button>
         <!-- Indicates a successful or positive action -->
         <button type="button" class="btn btn-success">Success</button>
         <!-- Contextual button for informational alert messages -->
         <button type="button" class="btn btn-info">Info</button>
         <!-- Indicates caution should be taken with this action -->
         <button type="button" class="btn btn-warning">Warning</button>
         <!-- Indicates a dangerous or potentially negative action -->
         <button type="button" class="btn btn-danger">Danger</button>
         <!-- Deemphasize a button by making it look like a link while maintaining button behavior -->
         <button type="button" class="btn btn-link">Link</button>
      </div>
   </body>
</html>

결과보기 : https://jsfiddle.net/ysohpdqh/5/?utm_source=website&utm_medium=embed&utm_campaign=ysohpdqh

bootstrap-button-sample

컴포넌트

부트스트랩에서는 iconography, dropdown, input group, navigation, alerts 등의 재사용 가능한 컴포넌트를 제공하고 있다. 본 가이드에서는 몇 가지 컴포넌트만 소개한다.

그 밖의 다른 컴포넌트 들은 링크를 통해 확인 하도록 한다.

Glyphicons

250개 이상의 기호가 Glyphicon Halflings 세트로 폰트 포맷에 포함되어 있다.

bootstrap-glyphicons

사용방법

사용할 부분에 다음 구문을 삽입한다.

<span class="glyphicon glyphicon-search" aria-hidden="true"></span>

위의 구문에서 원하는 Glyphicon의 class를 교체하여 사용한다.

다음은 Glyphicon을 사용한 예제이다.

<!DOCTYPE html>
<html lang="ko">
   <head>
      <title>Bootstrap Example</title>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" 
         integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
      <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" 
         integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
   </head>
   <body>
      <div class="container">
         <h2>Glyphicon Examples</h2>
         <p>Envelope icon: <span class="glyphicon glyphicon-envelope"></span></p>
         <p>Envelope icon as a link:
            <a href="#"><span class="glyphicon glyphicon-envelope"></span></a>
         </p>
         <p>Search icon: <span class="glyphicon glyphicon-search"></span></p>
         <p>Search icon on a button:
            <button type="button" class="btn btn-default">
            <span class="glyphicon glyphicon-search"></span> Search
            </button>
         </p>
         <p>Search icon on a styled button:
            <button type="button" class="btn btn-info">
            <span class="glyphicon glyphicon-search"></span> Search
            </button>
         </p>
         <p>Print icon: <span class="glyphicon glyphicon-print"></span></p>
         <p>Print icon on a styled link button:
            <a href="#" class="btn btn-success btn-lg">
            <span class="glyphicon glyphicon-print"></span> Print 
            </a>
         </p>
      </div>
   </body>
</html>

결과보기 : https://jsfiddle.net/t1tresaj/1/?utm_source=website&utm_medium=embed&utm_campaign=t1tresaj

bootstrap-glyphicons-sample

페이지네이션(Pagination)

페이지네이션 컴포넌트로 사이트나 앱을 위한 페이지네이션 링크를 제공한다.

사용방법

페이지네이션의 경우 순서가 없는 목록(unordered list, <ul> )에 .pagination class 클래스를 추가한다.

다음은 페이지네이션을 사용한 예제이다.

<!DOCTYPE html>
<html lang="ko">
   <head>
      <title>Bootstrap Example</title>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" 
         integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
      <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" 
         integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
   </head>
   <body>
      <div class="container">
         <h2>Pagination - Active State</h2>
         <p>Add class .active to let the user know which page he/she is on:</p>
         <ul class="pagination">
            <li><a href="#">1</a></li>
            <li class="active"><a href="#">2</a></li>
            <li><a href="#">3</a></li>
            <li><a href="#">4</a></li>
            <li><a href="#">5</a></li>
         </ul>
      </div>
   </body>
</html>

결과보기 : https://jsfiddle.net/L860z1cy/1/?utm_source=website&utm_medium=embed&utm_campaign=L860z1cy

bootstrap-pagination-sample

위의 예제에서 리스트 아이템(list item, <li>)에 .active class를 추가하면, 해당 링크는 활성 상태가 된다.

클릭할 수 없는 링크일 경우 .disable을 사용하면 비활성 상태로 된다.

자바스크립트

부트스트랩 내에 존재하는 UI 컴포넌트에 동적인 인터랙션이 필요한 컴포넌트는 12개가 넘는 jQuery plugin을 통하여 컨트롤 할 수 있도록 되어있다.

단 컴포넌트를 주의할 점은 모든 플러그인은 jQuery에 의존하기 때문에 jQuery는 반드시 플러그인 파일 전에 포함되어야 한다.

본 가이드에서는 모달(modal)에 대한 가이드만 제공한다.

모달(Modal)

모달 플러그인은 현재 페이지의 상단에 표시되는 대화 상자 / 팝업 창이다.

  • 다른 모달이 보이는 동안에 모달은 열리지 않는다.
  • 다른 컴포넌트가 모달의 모습이나 기능에 영향을 끼치지 않도록 항상 모달의 HTML 코드를 문서 상단에 위치한다.
  • 모바일 기기에서는 제약사항이 있다.
  • 플러그인 사용 시 개별적(modal.js)으로 include하거나, 전체(bootstrap.js 또는 bootstrap.min.js)를 include하여 사용할 수 있다.
사용법

다음은 기본적인 modal을 만드는 방법을 보여주는 예제이다.

<!DOCTYPE html>
<html lang="ko">
   <head>
      <title>Bootstrap Example</title>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" 
         integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
      <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" 
         integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
   </head>
   <body>
      <div class="container">
         <h2>Modal Example</h2>
         <!-- Trigger the modal with a button -->
         <button type="button" class="btn btn-info btn-lg" data-toggle="modal" data-target="#myModal">Open Modal</button>
         <!-- Modal -->
         <div class="modal fade" id="myModal" role="dialog">
            <div class="modal-dialog">
               <!-- Modal content-->
               <div class="modal-content">
                  <div class="modal-header">
                     <button type="button" class="close" data-dismiss="modal">&times;</button>
                     <h4 class="modal-title">Modal Header</h4>
                  </div>
                  <div class="modal-body">
                     <p>Some text in the modal.</p>
                  </div>
                  <div class="modal-footer">
                     <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                  </div>
               </div>
            </div>
         </div>
      </div>
   </body>
</html>

결과보기 : https://jsfiddle.net/eqfmxd3e/1/?utm_source=website&utm_medium=embed&utm_campaign=eqfmxd3e

bootstrap-modal-sample

위의 예제의 구조를 보면 크게 Triger, Modal, Modal content로 구성된 것을 확인할 수 있다.

  • Trigger
    • 모달 윈도우를 호출하려면 트리거가 필요하다.
    • 보통 버튼이나 링크를 트리거로 사용한다.
    • 두 개의 data-* 속성을 include 해야 한다.
      • data-toggle=modal : 모달 윈도우를 연다.
      • data-target=#myModal : 모달 윈도우를 열 id
  • Modal
    • 모달의 부모 <div>에는 모달(myModal)을 트리거 하기 위해 사용되는 data-target의 속성 값과 동일한 ID를 가져야 한다.
    • .modal 클래스는 content에서 modal의 <div>를 식별하고, 그것을 focus로 가져온다.
    • .fade 클래스는 모달 안과 밖으로 fade 전환 효과를 추가해 준다. 만약 효과를 사용하지 않으면 이 클래스를 삭제한다.
    • role=dialog 속성은 스크린 리더를 사용하는 사람들에 대한 접근성을 향상시킨다.
    • .modal-dialog 클래스는 모달의 적절한 폭과 여백을 설정한다.
  • Modal content
    • modal-content 클래스의 <div>는 모달의 스타일(border, background-color 등…)을 지정한다. 여기에 모달의 header, body, footer를 추가한다.
    • .modal-header 클래스는 모달의 header 스타일을 정의하는데 사용한다.
    • header 안의 <button>에는 data-dismiss=modal 속성이 정의되어 있는데 이는 클릭하면 모달을 닫는 기능을 수행한다.
    • .close 클래스는 닫기 버튼의 스타일이다.
    • .modal-title 클래스는 header의 적절한 라인 높이의 스타일이다.
    • .modal-body 클래스는 모달의 body 스타일을 정의하는데 사용한다. 여기에 다른 HTML markup(paragraphs, images, videos, 등…)들을 추가한다.
    • .modal-footer 클래스는 모달의 footer 스타일을 정의하는데 사용한다. 기본적으로 우측 정렬로 되어있다.

참고자료

3.30 - UX/UI Controller Component

전자정부는 스마트 전자정부 기반 시스템 구축을 위해 UI/UX Controller Component, HTML5, CSS3, JavaScript Module App Framework를 활용하며, jQuery Mobile을 오픈소스로 채택하여 이를 커스터마이징한다. UI 레이어에서는 터치 최적화된 UI 컨트롤러 컴포넌트와 모바일 특화 HTML5 태그, CSS3를 통해 유연한 사용자 환경을 제공하며, JavaScript와 JSON 구조로 효율적인 UX/UI 컨트롤을 지원한다.

UX/UI Controller Component

개요

전자정부에서 효율적인 스마트 전자정부 기반시스템의 구축•운영을 통해 전자정부의 서비스 품질 UX 레이어는 UI/UX Controller Component, JavaScript Module App Framework, HTML5, CSS3 서비스를 제공한다. 오픈소스는 JQuery Mobile을 채택하였으며 jQuery Mobile은 html5, CSS3, javascript를 제공한다. 오픈 소스를 Customizing 하여 UI레이어의 기능을 사용 하며 내용은 아래와 같다 UI/UX Controller Component 모바일 웹 사용자 환경(UX/UI)에 대한 유연한 대응을 위해 Touch Optimized 된 필수 UI 컨트롤러 컴포넌트를 제공한다. HTML5는 모바일 웹 페이지 구성 시 사용 할 수 있는 마크업 언어로서 모바일 특화 태그 밑 디바이스 API를 제공한다. CSS3는 모바일 기기 및 브라우저에 따라 적합한 컴포넌트가 보여지는 기능을 제공한다. 또한 JavaScript Module App Framework UX/UI controller component의 효율성을 보장하는 javascript 밑 Json 구조를 제공한다.

설명

UX 처리 레이어는 모바일 환경을 화면을 담당하는 레이어로 화면 구성을 위한 Button, Panel, Internal/Externel Link, Process Dialog/Bar, Menu, Date/Time Picker, Check, Radio, Label/Text, TABS, Form, Grid, List View ICon, Selector, Collapsible Block를 제공한다.모바일 화면에 특화된 16개의 컴포넌트를 제공한다

모바일 페이지 선언

jQuery Mobile 은 HTML5의 doctype 으로 선언하여야 하며, jQueryMobile에서 사용하는 CSS, JS(jQuery, jQueryMobile)를 Import 함으로서 사용 할 수 있다.

jQuery Mobile은 jQuery Core를 사용하고 있다.

모바일 Page Header

Page의 Header 선언 부분에 모바일 실행환경을 import한다.

<!DOCTYPE html>
<html>
 <head>
  <title>eGovFrame</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <link rel="stylesheet" href="/css/egovframework/mbl/cmm/jquery.mobile-1.3.2.css"/>
  <link rel="stylesheet" href="/css/egovframework/mbl/cmm/EgovMobile-1.3.2.css" />
  <script src="/js/egovframework/mbl/cmm/jquery-1.9.1.min.js"></script>
  <script src="/js/egovframework/mbl/cmm/jquery.mobile-1.3.2.min.js"></script>
  <script src="/js/egovframework/mbl/cmm/EgovMobile-1.3.2.js"></script>
 </head>
 <body>
 ...
 </body>
</html>

모바일 Page Body

jQuery Mobile 의 Page 구조는 div를 사용하여 표현하며 html5의 ‘data-*’ 속성을 이용하여 구조를 구분한다.

<div data-role=“page”>
 <div data-role=“header”>
 </div>
 <div data-role=“content”>
 </div>
 <div data-role=“footer”>
 </div>
</div>

jQuery Mobile 은 하나의 페이지를 <div data-role=“page”> 단위로 관리 하며 한 HTML 내에 여러 <div data-role=“page”> 가 있을 경우 제일 상단의 div page를 첫 화면으로 인식한다. 이들 내부 페이지 간 이동은 링크 속성에 #pageName을 사용해서 가능 하다. jQuery Mobile 은 외부 페이지 이동시 anchor 태그의 링크를 가로채서 Ajax 로 해당 URL 호출 후 호출 된 Page 의 <div data-role=“page”> 영역만 가져와서 호출 한 HTML 페이지의 DOM 에 해당 내용을 추가 한다.

  • Ajax 로 호출된 page 의 CSS, JS 는 가져 오지 않기 때문에 호출한 Page는 호출된 Page 의 JS, CSS를 포함하고 있어야 한다.
  • 외부페이지 이동 시 ajax 통신을 하고 싶지 않은 경우에는 data-ajax=“false”를 사용한다.

UX Component

컴포넌트제공기능
Button설명: 명령 수행, 옵션 선택, 다른 대화 상자 열기 등에 사용하는 컴포넌트 제공
형태: 둥근 형(radius: 1em), 사각형(radius: 0em)
배치: vertical group, horizontal group
색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록
높이: normal(39px), small (28px)
넓이: 화면에 맞게 , 텍스트에 맞게
Panel설명: Header/footer 와 함께 페이지를 구성하는 요소
무늬: 격자 형태 무늬 제공
색상: 검정, 회색, 연한회색, 흰색, 노랑, 빨강, 초록
Internal / External Link설명: 표준 링크 기능을 제공하며 기본적으로 Ajax 를 사용한 링크 방식 제공
링크: 페이지 내부링크, 도메인 내부 링크, 외부 링크, 이메일 링크, 폰 링크, 에러 페이지 링크
Label / Text설명: 색상, 배치, 크기, 폰트를 지정 할 수 있는 가이드 제공
색상: 초록, 빨강, 파랑
배치: 왼쪽, 중간, 오른쪽
크기: 15px, 25px, 30px
폰트: helvetica, verdana, tahoma
Tabs설명: Header와 footer 에 사용되며 탭 버튼으로 문서간 이동 기능 제공
형태: round tab(radius: 0.250em), normal tab(radius: 0em)
배치: 1, 1/2, 1/3, 1/4, 1/5, 1/2 다중행 tab
색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록
Form설명: HTML Form 요소를 모바일 환경에 최적화하여 제공
요소: Text inputs, Search inputs, Sliders, Switches, Radio buttons, Check boxes, Selectors
색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록
Menu설명: Dialog, Grid, List, Collapsible 컴포넌트를 사용하여 메뉴 구성 기능 제공
효과: slide, slideup, slidedown, pop, fade, flip, turn, flow, slidefade
형태: Dialog, Grid, List, Collapsible
색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록
Processing Dialog / Bar설명: 페이지 전환 간 진행 상태를 확인 할 수 있는 Progress Dialog/Bar 제공
형태: Processing Dialog, Processing Bar
색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록
Dialog설명: 사용자와 상호작용할 수 있는 Dialog 기능 제공
형태: Dialog, Action Sheet, Overlay, Alert, Prompt, Confirm
색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록
Grid View설명: Grid 형태로 컨텐츠를 배치할 수 있는 컴포넌트 제공
배치: 1/2, 1/3, 1/4, 1/5, 가변 Grid View
Table / List View설명: Table/List 형태로 컨텐츠를 배치할 수 있는 컴포넌트 제공
형태: Read-only list, Link list
기능: Nested List, Numbered List, Split Button, List Divider, Count Bubble, Thumbnail, List icon, Content Formatting, Search Filter Bar, Change Mode List, Checked List
색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록
Date / Time Picker설명: 날짜와 시간을 선택할 수 있는 Picker 제공
형태: Android Date Picker, Popup Calendar, Android Time Picker, Flip Picker(Date, Time)
Check/ Radio설명: Check/Radio 형태로 항목을 선택할 수 있는 기능 제공
배치: vertical group, horizontal group
색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록
Icon설명: 모바일 어플리케이션에 가장 많이 사용되는 아이콘 제공
형태: arrow-l, arrow-r, arrow-u, arrow-d, delete, plus, minus, check, gear, refresh, forward, back, grid, star, alert, info, search, home, phone, mail, gps, audio, camera, file, mic, explorer
색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록
Selector / Switch설명: Selector/Switch 형태로 항목을 선택할 수 있는 기능 제공
효과: pop-up, list
기능: 다중선택, 단일 선택
모양: 둥근 형(radius: 1em), 사각형(radius: 0em)
넓이: 화면에 맞게, 텍스트에 맞게
효과: Shadow 적용, Shadow 제거
형태: 비그룹, 그룹
색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록
Collapsible Block설명: 콘텐츠 영역을 접었다 펼 수 있는 컨트롤 기능 제공
형태: normal, Group, Nested
색상: 검정, 파랑, 회색, 흰색, 노랑, 빨강, 초록

UX 컴포넌트별 브라우저 호환성 준수 지침 기준

  1. 근거 기준: 전자정부서비스 호환성 준수지침(행정안전부고시 제2010-40호, 2010. 6. 24)
  2. 개정 이유: 모바일 전자정부 서비스 제공시 접근성 제고등을 위하여 공공기관이 준수해야 할 사항을 규정
  3. 주요 개정 내용
    • 국민들이 다양한 모바일 기기를 사용할 수 있도록 모바일 ‘앱’방식보다 모바일’웹’방식을 권고
    • 모바일 웹 방식 개발을 위한 기술 표준 지침
      • 최소3종 이상 웹 브라우저에서 동등한 서비스 제공
      • 국제 표준화 기구에서 제공하는 표준 사용 의무화
  4. 전자정부 모바일 서비스 제공 원칙
    • 다양한 스마트폰 사용자들이 모두 혜택을 받을 수 있도록 모바일 웹 방식의 개발을 권장
    • 모바일 공통컴포넌트 별 지원 브라우저(호환성) 참조

UX 컴포넌트 별 지원 브라우저 (호환성)

테스트 디바이스

img_test_device

테스트 브라우저

test_browser

모바일 표준프레임워크 사용자경험(UX)지원 브라우저 내용

table_support

참고자료

모바일 실행환경 사용자경험(UX)지원기능 가이드

3.31 - HTML5 CSS3.0 JavaScript Module App Framework 기본 활용

전자정부는 효율적인 스마트 전자정부 시스템 구축을 위해 UI/UX Controller Component, HTML5, CSS3, JavaScript Module App Framework를 제공하며, 이 과정에서 jQuery Mobile을 활용하여 모바일 웹 사용자 경험을 최적화한다. Ajax 기반의 페이지 이동 및 다양한 이벤트와 메서드를 지원하여 반응형 모바일 애플리케이션 개발을 용이하게 한다.

HTML5 CSS3.0 JavaScript Module App Framework 기본 활용

개요

전자정부에서 효율적인 스마트 전자정부 기반 시스템의 구축•운영을 위해 전자정부의 서비스 품질 UX 레이어로 UI/UX Controller Component, HTML5, CSS3, JavaScript Module App Framework 서비스를 제공한다. 오픈소스는 JQuery Mobile을 채택하였으며 jQuery Mobile은 html5, CSS3, javascript를 제공한다. 이를 Customizing 하여 UI 레이어의 기능을 사용하며 내용은 아래와 같다.

  • UI/UX Controller Component : 모바일 웹 사용자 환경(UX/UI)에 대한 유연한 대응을 위해 Touch Optimized 된 필수 UI 컨트롤러 컴포넌트를 제공한다.
  • HTML5 : 모바일 웹 페이지 구성 시 사용할 수 있는 마크업 언어로서 모바일 특화 태그 및 디바이스 API를 제공한다.
  • CSS3 : 모바일 기기 및 브라우저에 따라 적합한 컴포넌트가 보이게 하는 기능을 제공한다.
  • JavaScript Module App Framework : UX/UI controller component의 효율성을 보장하는 javascript 및 Json 구조를 제공한다.

설명

기본 활용 구조

전자정부 모바일 표준프레임워크 실행환경은 기존 전자정부 표준프레임워크의 디렉터리 구조 및 표준을 준수하고 있으며, 모바일 웹 개발에 편의를 제공하기 위해 하위 디렉터리 구조를 다음과 같이 구성하고 있다.

  • 프로젝트의 하위 폴더인 ‘src’에 실행환경을 지원하는 라이브러리 및 JSP 파일이 존재한다. 라이브러리는 CSS, JavaScript 및 이미지 파일로 구성되어 있다.

    전자정부 모바일 표준프레임워크 실행환경 라이브러리 경로 이미지

  • 전자정부 모바일 표준 프레임워크는 CSS 및 JavaScript를 이용하여 실행환경을 제공하며 CSS, javascript, image는 각각 유기적으로 연결되어 있다.

    전자정부 모바일 표준프레임워크 실행환경의 CSS, JavaScript, Images 설명

  • HTML5 패턴인 ‘data-role’ 속성에 적용된 값에 따라 Page, Header, Content, Footer 영역으로 구분된다.

    전자정부 모바일 표준프레임워크 실행환경의 영역 설명

javascript 구조와 Ajax 처리

모바일 페이지 이동은 기본적으로 Ajax를 이용하여 처리된다. 이는 모바일에 최적화된 화면 전환 효과를 주기 위함으로 옵션 설정을 통해 변경 가능하다.

  • 페이지 내부 이동

    • 하나의 HTML 파일 안에 여러 page가 선언되어 있는 경우에 사용할 수 있는 방법으로 모바일 page 구성의 기본 방식이다.

    • 페이지 내부 이동은 Ajax 통신을 사용하며 page로 선언된 div 태그의 id 값을 링크의 href 속성 값(#pageId)으로 적용하여 사용 가능하다. (한 HTML 내에 여러 page가 선언되어 있을 경우 제일 상단의 page를 첫 화면으로 인식한다.)

    • 페이지 내부 이동은 Ajax 방식을 기본으로 하기 때문에 연속적으로 여러 번 사용하면 DOM 객체를 제대로 못 불러 올 경우가 있으므로 외부 페이지 이동을 권장한다.

      전자정부 모바일 표준프레임워크 실행환경 페이지 내부 이동

  • 페이지 외부 이동

    • 페이지 외부 이동은 Ajax 통신을 이용하며 Ajax로 호출한 html의 data-role=“page” 영역만 읽어 들여서 호출한 html 페이지의 DOM 요소에 추가해 준다. (페이지 내부 이동과 유사 한 구조로 DOM 관리)
      • Ajax로 호출된 HTML의 page 영역만 가져오기 때문에 호출된 페이지에서 사용하는 JavaScript, CSS 등은 호출을 한 HTML 내에 존재해야 한다.

      • Ajax 통신을 사용하고 싶지 않은 경우 Internal / External UX Component를 참조하여 변경 가능하다.

        전자정부 모바일 표준프레임워크 실행환경 페이지 외부 이동

  • mobileinit 이벤트와 기본 환경 설정

    • 전자정부 모바일 표준프레임워크는 모바일 애플리케이션이 시작될 때 각종 초기화 작업이 수행될 수 있도록 mobileinit 이벤트를 통하여 기본 환경 설정을 변경할 수 있도록 한다.
    • mobileinit 이벤트는 page가 시작되자마자 발생하는 이벤트로 첫 번째 초기화 작업 시 실행되며, 함수 내부에 여러 이벤트를 적용하여 사용할 수 있다.
      $(document).bind("mobileinit", function(){
            //apply overrides here
      });
      
    • mobileinit 이벤트는 실행 즉시 발생하므로 jquerymoible.js가 로드되기 전에 바인딩 되어야 한다. 다시 말해 mobileinit 이벤트의 위치는 jquery 라이브러리와 jquerymobile 라이브러리 사이에 위치 해야 한다.
      <script src="jquery.js"></script>
      <script src="custom-scripting.js"></script>
      <script src="jquery-mobile.js"></script>
      
  • $.mobile 객체를 통해 재설정이 가능한 주요 기본 환경설정

    기본 환경설정설명
    loadingMessage (string, default: “loading”)페이지가 로딩될 때 나타나는 텍스트를 설정한다. ‘false’로 설정하면 로딩 메시지가 나타나지 않는다.
    pageLoadErrorMessage (string, default: “Error Loading Page”)Ajax 방식의 페이지 이동에서 페이지를 로드하지 못했을 경우 나타나는 에러 메시지의 텍스트를 설정한다.
    defaultDialogTransition (string, default: ‘pop’)다이얼로그에서 Ajax 방식을 통한 페이지 전환에 관여하는 기본 환경설정을 변경한다. defaultDialogTransition 옵션을 ‘none’으로 설정하면 화면전환 효과가 적용되지 않는다.
    defaultPageTransition (string, default: ‘slide’)Ajax 방식을 사용하는 페이지 전환에 관여하는 기본 환경설정을 변경한다. defaultPageTransition 옵션을 ‘none’으로 설정하면 화면전환 효과가 적용되지 않는다.
    ajaxEnabled (boolean, default: true)모든 링크 이동이나 폼 전송은 기본적으로 Ajax 방식을 기반으로 하고 있다. Ajax가 아니라 일반 방식으로 페이지 이동을 처리하고 싶다면 이 값을 ‘false’로 지정한다.
  • 이벤트

    • 전자정부 모바일 표준프레임워크는 스마트 기반 모바일 환경에 적합한 이벤트를 선별하여 제공한다. Touch, Mouse, Window 영역의 다양한 이벤트를 지원 가능 여부에 따라 선택적으로 이용하기 때문에 모바일 환경과 데스크톱(Desktop) 환경 모두에서 사용 가능하다. live() 또는 bind() 메서드를 이용하여 여러 이벤트를 함께 사용할 수 있다.

    • 지원 터치 이벤트

      터치 이벤트설명
      tapTouch가 감지되면 즉시 발생하는 이벤트이다.
      tapholdtap을 일정 시간 이상 지속했을 때 발생하는 이벤트이다.
      swipe30pixel 이상의 수평 방향이나 20pixel 이상의 수직 방향으로 드래그(drag) 되면 발생하는 이벤트이다.
      swipeleftswipe 이벤트가 왼쪽으로 일어났을 때 발생하는 이벤트이다.
      swiperightswipe 이벤트가 오른쪽으로 일어났을 때 발생하는 이벤트이다.
    • 지원 화면 방향 전환 및 스크롤 이벤트

      화면 방향 전환 및 스크롤 이벤트설명
      orientationChange기기의 방향이 수평 또는 수직으로 바뀌었을 때 발생하는 이벤트이다. orientationChange 이벤트가 지원되지 않을 경우에는 resize 이벤트가 자동으로 bind 된다.
      scrollstart스크롤(scroll)이 시작되면 발생하는 이벤트이다. (iOS 기기는 스크롤 중에는 DOM 을 변경하지 않고 queue에 저장해두었다가 스크롤이 끝난 후에 변경한다.)
      scrollstop스크롤이 끝나면 발생하는 이벤트이다.
    • 지원 페이지 이벤트

      페이지 이벤트설명
      pagebeforecreate페이지가 초기화되기 직전에 발생하며 페이지 로딩 시 가장 먼저 발생하는 이벤트이다. 페이지 생성 시에만 이벤트가 발생한다.
      pagecreate페이지 초기화가 끝나면 발생하는 이벤트이다. 페이지 생성이 완료된 시점에만 이벤트가 발생한다.
      pagebeforeshow화면전환이 일어나기 전, 즉 페이지가 보이기 전에 매번 발생하는 이벤트이다.
      pageshow화면전환이 완료되었거나 페이지가 보인 후에 매번 발생하는 이벤트이다.
      pagebeforehide화면전환이 일어나기 전, 즉 페이지가 숨겨지기 전에 매번 발생하는 이벤트이다.
    • Visual Mouse event

      페이지 이벤트설명
      vmouseover터치 이벤트 또는 mouseover가 발생할 때 나타나는 이벤트이다.
      vmousedown터치 이벤트 또는 mousedown이 발생할 때 나타나는 이벤트이다.
      vmousemove터치 이벤트 또는 mousemove가 발생할 때 나타나는 이벤트이다.
      vmouseup터치 이벤트 또는 mouseup이 발생할 때 나타나는 이벤트이다.
  • 메서드 & 유틸리티

    • 전자정부 모바일 표준프레임워크는 $.mobile 객체에 대한 메서드와 속성들을(properties) 제공한다.
      메서드설명
      $.mobile.changePage(method)프로그램 코드 상에서 페이지를 이동할 수 있도록 지원하는 메서드이다. 주로 화면전환, 페이지 로딩 등의 기능이 가능한 환경에서 링크 클릭이나 폼 전송을 할 때 내부적으로 사용된다.
      $.mobile.loadPage(method)외부 페이지를 로드하고, DOM에 추가한다. 이 메서드는 첫 번째 인자로 URL이 올 때 changePage() 함수를 통해 내부적으로 호출된다. 이 함수는 현재 활성화된 페이지에는 영향을 주지 않고, 백그라운드에서 페이지를 로드 할 때 사용된다.
      $.mobile.loading(“show”)페이지 로딩 메시지를 보여준다.
      $.mobile.loading(“hide”)페이지 로딩 메시지를 숨긴다.

참고자료

4 - 업무처리

업무처리 서비스는 업무 프로그램의 업무 로직을 담당하는 서비스로 업무 흐름제어, 에러 처리 등의 기능을 제공한다.

업무처리

업무처리 서비스는 업무 프로그램의 업무 로직을 담당하는 서비스로 업무 흐름제어, 에러 처리 등의 기능을 제공한다.

4.1 - Exception Handling 서비스

전자정부 표준프레임워크 기반 시스템에서 Exception 처리는 AOP를 이용해 비즈니스 로직과 분리된 After throwing advice로 정의되며, Exception에 따른 적절한 대응을 목표로 한다. Exception 발생 시 클래스 정보와 종류는 후처리 로직 적용 여부를 결정하는 중요한 요소이다.

Exception Handling 서비스

개요

전자정부 표준프레임워크 기반의 시스템 개발 시 Exception 처리, 정확히는 Exception별 특정 로직(후처리 로직이라고 부르기도 함)이 흐를 수 있도록 하여 Exception에 따른 적절한 대응이 가능하도록 하고자 하는데 목적이 있다.
AOP의 도움을 받아 비즈니스 POJO와 분리되어 After throwing advice로 정의하였다.
AOP 관련 내용은 AOP 모듈을 참조하길 바란다.

Exception에 대해 이야기 하겠다.
Exception 발생 시 Exception 발생 클래스 정보와 Exception 종류가 중요하다.
Exception 발생 클래스 정보와 Exception 종류는 모두 후처리 로직의 대상일지 아닐지를 결정하는 데 사용된다.

	public CategoryVO selectCategory(CategoryVO vo) throws Exception {
		CategoryVO resultVO = categoryDAO.selectCategory(vo);
		try {
    ....
 
    // 넘어온 resultVO가 null 인 경우 EgovBizException 발생 (result.nodata.msg는 메세지 키에 해당됨)
		if (resultVO == null)
			throw processException("result.nodata.msg"); 
            // 또는 throw processException("result.nodata.msg", 발생한 Exception );
		return resultVO;
	}

설명

앞에서 언급했던 Exception 후처리 방식과 Exception은 아니지만 후처리 로직(leaveaTrace)을 실행할 하는 방식에 대해 설명하도록 하겠다.
간략하게 보면 Exception 후처리 방식은 AOP(pointCut ⇒ after-throw) ⇒ ExceptionTransfer.transfer() ⇒ ExceptionHandlerService ⇒ Handler 순으로 실행된다.

LeavaTrace는 AOP를 이용하는 구조가 아니고 Exception을 발생하지도 않는다. 단지 후처리 로직을 실행하도록 하고자 함에 목적이 있다.
실행 순서는 LeavaTrace ⇒ TraceHandlerService ⇒ Handler 순으로 실행한다.

먼저 Exception Handling에 대해 알아보도록 하자.

Aop Config, ExceptionTransfer 설정 및 설명

Bean 설정

Exception 후처리와 leaveaTrace 설정을 위해서 샘플에서는 두 개의 xml 파일을 이용한다. (context-aspect.xml, context-common.xml)

먼저 Exception 후처리를 위한 부분을 보겠다.
Exception Handling을 위한 AOP 설정은 아래와 같다.
비즈니스 개발 시 패키지 구조는 바뀌기 때문에 Pointcut은 egov.sample.service.*Impl.*(..))을 수정하여 적용할 수 있다.
ExceptionTransfer의 property로 존재하는 exceptionHandlerService는 다수의 HandleManager를 등록 가능하도록 되어 있다.
여기서는 defaultExceptionHandleManager을 등록한 것을 볼 수 있다.

context-aspect.xml
...
	<aop:config>
		<aop:pointcut id="serviceMethod"
			expression="execution(* egov.sample.service.*Impl.*(..))" />
 
		<aop:aspect ref="exceptionTransfer">
			<aop:after-throwing throwing="exception"
				pointcut-ref="serviceMethod" method="transfer" />
		</aop:aspect>
	</aop:config>
 
	<bean id="exceptionTransfer" class="egovframework.rte.fdl.cmmn.aspect.ExceptionTransfer">
		<property name="exceptionHandlerService">
			<list>
				<ref bean="defaultExceptionHandleManager" />
			</list>
		</property>
	</bean>
 
	<bean id="defaultExceptionHandleManager"
		class="egovframework.rte.fdl.cmmn.exception.manager.DefaultExceptionHandleManager">
		<property name="patterns">
			<list>
				<value>**service.*Impl</value>
			</list>
		</property>
		<property name="handlers">
			<list>
				<ref bean="egovHandler" />
			</list>
		</property>
	</bean>
 
	<bean id="egovHandler"
		class="egovframework.rte.fdl.cmmn.exception.handler.EgovServiceExceptionHandler" />
...

defaultExceptionHandleManager는 setPatterns(), setHandlers() 메소드를 가지고 있다. 상단과 같이 등록된 pattern 정보를 이용하여 Exception 발생 클래스와의 비교하여 ture인 경우 handlers에 등록된 handler를 실행한다.
패턴 검사 시 사용되는 pathMatcher는 AntPathMatcher를 이용하고 있다.

특정 pattern 그룹군을 만든후 patterns에 등록하고 그에 해당하는 후처리 로직을 정의하여 등록할 수 있는 구조이다.

Handler 구현체

먼저 클래스에 대한 이해가 필요하다.
앞단에서 간단하게 설명했지만 다시 정리 하자면 Exception 발생 시 AOP pointcut “After-throwing”에 걸려 ExceptionTransfer 클래스의 transfer가 실행된다.
transfer 메소드는 ExceptionHandlerManager의 run 메소드를 실행한다.
아래는 구현 예로 DefaultExceptionHandleManager 코드이다.
(구현 시 필수사항) 상위클래스는 AbsExceptionHandleManager 이고 인터페이스는 ExceptionHandlerService 이다.
구현되는 메소드는 run(Exception exception)인 것을 확인할 수 있다.

DefaultExceptionHandleManager.java
public class DefaultExceptionHandleManager extends AbsExceptionHandleManager implements ExceptionHandlerService {
 
	@Override
	public boolean run(Exception exception) throws Exception {
 
		log.debug(" DefaultExceptionHandleManager.run() ");
 
		// 매칭조건이 false 인 경우
		if (!enableMatcher())
			return false;
 
		for (String pattern : patterns) {
			log.debug("pattern = " + pattern + ", thisPackageName = " + thisPackageName);
			log.debug("pm.match(pattern, thisPackageName) =" + pm.match(pattern, thisPackageName));
			if (pm.match(pattern, thisPackageName)) {
				for (ExceptionHandler eh : handlers) {
					eh.occur(exception, getPackageName());
				}
				break;
			}
		}
 
		return true;
	}
 
}

Customizable Handler 등록

시나리오 : CustomizableHandler 클래스를 만들어 보고 sample 패키지에 있는 Helloworld 클래스 Exception 시에 CustomizableHandler를 실행한다.
먼저 CustomHandler 클래스를 아래와 같이 만든다.
ExceptionHandleManager 에서는 occur 메소드를 실행한다.
Handler 구현체는 반드시 (필수사항) ExceptionHandler 인터페이스를 구현해야 한다.

CustomizableHandler.java
public class CustomizableHandler implements ExceptionHandler {
 
	protected Log log = LogFactory.getLog(this.getClass());
 
	public void occur(Exception ex, String packageName) {
 
		log.debug(" CustomHandler run...............");
		try {
			log.debug(" CustomHandler 실행 ...  ");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

CustomizableHandler의 등록을 해보도록 하겠다.
여기서 주의해야 하는 부분은 patterns에 sample 패키지에 있는 Helloworld 클래스를 지정해주어야 한다는 것이다.

	<bean id="exceptionTransfer" class="egovframework.rte.fdl.cmmn.aspect.ExceptionTransfer">
		<property name="exceptionHandlerService">
			<list>
				<ref bean="customizableExceptionHandleManager" />
			</list>
		</property>
	</bean>
 
	<bean id="customizableExceptionHandleManager"
		class="egovframework.rte.fdl.cmmn.exception.manager.DefaultExceptionHandleManager">
		<property name="patterns">
			<list>
				<value>**sample.Helloworld</value>
			</list>
		</property>
		<property name="handlers">
			<list>
				<ref bean="customizableHandler1" />
				<ref bean="customizableHandler2" />
				<ref bean="customizableHandler3" />
			</list>
		</property>
	</bean>
 
	<bean id="customizableHandler1" class="sample.CustomizableHandler" />
	<bean id="customizableHandler2" class="sample.CustomizableHandler" />
	<bean id="customizableHandler3" class="sample.CustomizableHandler" />

이런식으로 여러개의 Handler를 등록해줄 수 있다.

leaveaTrace 설정 및 설명

Exception이거나 Exception이 아닌 경우에 Trace 후처리 로직을 실행시키고자 할 때 사용한다.
설정하는 기본적인 구조는 Exception 후처리하는 방식과 같다. 설정 파일은 context-common.xml 이다.
DefaultTraceHandleManager에 TraceHandler를 등록하는 형태로 설정된다.

Bean 설정

...
	<bean id="leaveaTrace" class="egovframework.rte.fdl.cmmn.trace.LeaveaTrace">
		<property name="traceHandlerServices">
			<list>
				<ref bean="traceHandlerService" />
			</list>
		</property>
	</bean>
 
	<bean id="traceHandlerService" 	class="egovframework.rte.fdl.cmmn.trace.manager.DefaultTraceHandleManager">
		<property name="patterns">
			<list>
				<value>*</value>
			</list>
		</property>
		<property name="handlers">
			<list>
				<ref bean="defaultTraceHandler" />
			</list>
		</property>
	</bean>
 
	<bean id="antPathMatcher" class="org.springframework.util.AntPathMatcher" />
 
	<bean id="defaultTraceHandler"
		class="egovframework.rte.fdl.cmmn.trace.handler.DefaultTraceHandler" />
...

TraceHandler 확장 개발 Sample

Interface TraceHandler를 아래와 같이 implements 한다.
package egovframework.rte.fdl.cmmn.trace.handler;
 
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
 
 
public class DefaultTraceHandler implements TraceHandler {
 
 
    public void todo(Class clazz, String message) {
	//수행하고자 하는 처리로직을 넣는 부분...
	System.out.println(" log ==> DefaultTraceHandler run...............");
    }
 
}
leaveaTrace 코드상 발생 Sample

사용 방법을 다시 상기해보면 아래와 같다.
메세지 키(message.trace.msg)를 이용하여 메세지 정보를 넘겨 Handler를 실행한다.

	public CategoryVO selectCategory(CategoryVO vo) throws Exception {
		CategoryVO resultVO = categoryDAO.selectCategory(vo);
		try {
		  //강제로 발생한 ArithmeticException  
			int i = 1 / 0;
		} catch (ArithmeticException athex) {
		  //Exception을 발생하지 않고 후처리 로직 실행.
			leaveaTrace("message.trace.msg");
		}
 
		return resultVO;
	}

참고자료

4.2 - Spring Web Flow(SWF) 개요

Spring Web Flow는 웹 애플리케이션 내에서 논리적 페이지 흐름을 정의하고 수행하는 컴포넌트로, 단일 사용자의 대화를 비즈니스 프로세스와 함께 구현하여 안내한다. SWF는 Struts, Spring MVC 등과 통합되어 자족적인 페이지 흐름 엔진으로 작동하며, 선언적이고 관리하기 쉬운 방식으로 애플리케이션 흐름을 정의할 수 있다.

Spring Web Flow

개요

Spring Web Flow(SWF)는 웹 애플리케이션 내 페이지 흐름(flow)의 정의와 수행에 집중하는 Spring 프레임워크 웹 스택의 컴포넌트이다.

시스템은 다른 위치에서 재사용될 수 있는 자족적 모듈처럼 웹 애플리케이션의 논리적 흐름(flow)을 획득하는 것을 허용한다.
이러한 흐름(flow)은 비즈니스 프로세스의 구현을 통해 단일 사용자를 안내하고 단일 사용자 대화를 표현한다.
흐름(flow)은 종종 HTTP 요청을 처리하고 상태를 가지며, 트랜잭션 특성을 보이고 동적이고/이거나 장시간 구동될 수 있다.

Spring Web Flow는 추상화의 좀 더 높은 레벨에 존재하고 Struts, Spring MVC, Portlet MVC, 그리고 JSF와 같은 기본 프레임워크 내에서 자족적인 페이지 흐름(flow) 엔진(page flow engine)처럼 통합된다.
SWF는 선언적이고 높은 이식성을 가지며 뛰어난 관리능력을 가지는 형태로 명시적으로 애플리케이션의 페이지 흐름(flow)을 획득하는 기능을 제공한다.

설명

Spring Web Flow는 여타의 API에 대한 몇 가지 요구 의존성을 가진 자족적인 page flow engine처럼 구조화되었다. 모든 의존성은 주의 깊게 관리된다.

대부분의 사용자들은 좀 더 큰 웹 애플리케이션 개발 프레임워크 내 컴포넌트로 SWF를 끼워 넣을 것이다.
SWF는 요청 맵핑과 응답 표현을 다루기 위한 호출 시스템을 기대하는 컨트롤러 기술에 집중한다.
이 경우, 이러한 사용자는 환경을 위한 가는(thin) 통합 조각에 의존할 것이다.
예를 들어, Servlet 내 수행 흐름(flow)은 SWF에 대한 요청에 대한 할당(dispatch)과 SWF view 선택을 책임지는 표현을 다루는 Spring MVC 통합을 사용한다.

Spring과 마찬가지로, Spring Web Flow는 필요한 부분만 선택적으로 사용할 수 있는 계층화된(layered) 프레임워크로 패키징되어 있다.
SWF의 중요한 이득은 어떤 환경에서도 수행될 수 있는 자족적인 컨트롤러의 모듈을 재사용하여 정의할 수 있도록 하는 것이다.

구체적인 내용을 살펴보기 전에 Hello World를 실행해 보자.

Spring Web Flow의 기본 샘플로 Spring Source에서는 Hotel Booking 을 제공하고 있다.
우리는 Spring Web Flow 레퍼런스 문서를 기준으로 하고 샘플인 Hotel Booking 을 참고하는 형태로 설명하도록 하겠다.

Hotel Booking 샘플 데모 : 🌏 http://richweb.springframework.org/swf-booking-faces/spring/intro

SWF Configuration

SWF

참고 자료

4.3 -

4.4 - Spring Web Flow의 Hello World 예제

Spring Web Flow는 사용자와 서버 간의 대화 형식으로 화면 이동을 정의하며, “Hello, Web Flow” 화면을 호출하는 간단한 예제와 입력값을 받아 처리하는 예제로 나뉜다. 이 예제를 통해 SWF가 웹 대화형 시나리오에 어떻게 적용되는지 살펴본다.

Hello, World

개요

처음으로 접하므로 여기서는 Hello World를 찍어 보면서 실행하는 것을 살펴보도록 하겠다.
Hello World는 두 가지 버전으로 입력되는 값이 없이 단지 Hello, Web Flow 화면을 호출하는 것과 입력값을 가지고 분기 처리 등 서비스 메소드를 실행 후 결과를 화면으로 보여주는 버젼으로 나누어 설명하겠다. 실행하여 보고자 하는 화면 결과는 아래와 같다.

helloflow

설명

Spring Web Flow는 사용자와 Service를 제공하는 서버 간의 대화하듯한 화면의 이동을 정의하는 것이다.
SWF(Spring Web Flow)는 사용자와 화면 간의 대화 형태로 웹 대화형 시나리오를 중심으로 접근한다.

swfhelloworldcreate

web.xml

webContent/WEB-INF 아래 web.xml을 아래와 같이 작성한다.
contextConfigLocation의 값으로 /WEB-INF/config/web-application-config.xml을 설정한다.
servlet으로 org.springframework.web.servlet.DispatcherServlet를 등록하고 /spring/* URL 정보를 매핑해준다.

<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
	version="2.4">
 
	<!-- Spring Web 어플리케이션을 위한 메인 설정파일을 등록한다. -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>
			/WEB-INF/config/web-application-config.xml
		</param-value>
	</context-param>
 
	<!-- Spring Web 어플리케이션 컨테스트를 로딩한다. -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
 
	<!-- Spring Web 어플리케이션의 맨 앞단 Controller(DispatcherServlet) 를 등록한다. -->
	<servlet>
		<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value></param-value>
		</init-param>
		<load-on-startup>0</load-on-startup>
	</servlet>
 
	<!-- 모든 spring 요청에 대한되는 request를 DispatcherServlet 와 매핑하여 처리 할 수 있도록 한다. -->
	<servlet-mapping>
		<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
		<url-pattern>/spring/*</url-pattern>
	</servlet-mapping>
 
	<welcome-file-list>
		<welcome-file>index.html</welcome-file>
	</welcome-file-list>
 
</web-app>

web-application-config.xml

Spring MVC와 Spring Web Flow를 위한 설정 파일은 아래와 같다.

hellowebconfig

먼저 web-application-config.xml를 살펴보겠다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-2.5.xsd">
 
	<!-- 어플리케이션 소스를 스캔하여 로딩 하도록 한다. -->
	<context:component-scan base-package="org.egovframe.swf.sample.service" />
 
 
	<!-- 편의를 위하여 Spring MVC 설정과 Spring Web Flow  위한 설정을 별도록 분리하여 가져오도록 한다.--> 
	<import resource="webmvc-config.xml" />
	<import resource="webflow-config.xml" />
 
</beans>

webmvc-config.xml

Spring MVC를 위한 설정 파일

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
 
	<!--
	  flowRegistry에 등록된 flow와 요청되는 path와 매핑해주는 역할을 수행한다. 
		예제에선 요청되는 .../swfHelloWorld/spring/sample/hello  URL 정보를 이용하여 flow 내에서 sample/hello ID로 찾음.
	-->
	<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerMapping">
		<property name="order" value="0" />
		<property name="flowRegistry" ref="flowRegistry" />
	</bean>
 
	<bean
		class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping">
		<property name="order" value="1" />
		<property name="defaultHandler">
		  <!-- UrlFilenameViewController 는 spring/start 으로 접근하는 path 정보를 이용하여 
		   View 이름을 추출하여 View를 반환하게 된다. 여기서는 tiles의 view를 반환하게 된다. -->
			<bean class="org.springframework.web.servlet.mvc.UrlFilenameViewController" />
		</property>
	</bean>
 
	<!--
	  Controller에 의해 반환된 View 명을 tiles로 보내 tiles에 미리 정의된 화면을 보여주도록 한다.
	-->
	<bean id="tilesViewResolver" class="org.springframework.js.ajax.AjaxUrlBasedViewResolver">
		<property name="viewClass"
			value="org.springframework.webflow.mvc.view.FlowAjaxTilesView" />
	</bean>
 
	<!-- tiles 설정 정보를 정의한다. -->
	<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles2.TilesConfigurer">
		<property name="definitions">
			<list>
				<value>/WEB-INF/layouts/layouts.xml</value>
				<value>/WEB-INF/views.xml</value>
				<value>/WEB-INF/sample/views.xml</value>
				<value>/WEB-INF/sample/hello/views.xml</value>
			</list>
		</property>
	</bean>
 
 
	<!-- Dispatches requests mapped to POJO @Controllers implementations-->
	<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
 
	<!--
		Dispatches requests mapped to
		org.springframework.web.servlet.mvc.Controller implementations
	-->
	<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />
 
	<!--
		requests에 맞는 등록된 FlowHandler 구현부를 연결해준다. 
	-->
	<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter">
		<property name="flowExecutor" ref="flowExecutor" />
	</bean>
 
	<!-- Custom FlowHandler for the hello flow-->
	<bean name="sample/hello" class="org.egovframe.web.HelloFlowHandler" />
 
 
</beans>

webflow-config.xml

Web Flow 관련된 설정 파일

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:webflow="http://www.springframework.org/schema/webflow-config"
	xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
           http://www.springframework.org/schema/webflow-config
           http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.0.xsd">
 
	<webflow:flow-executor id="flowExecutor" />
 
	<!-- flow  정의한 파일을 가져와 flow registry 구성한다. -->
	<webflow:flow-registry id="flowRegistry"
		flow-builder-services="flowBuilderServices" base-path="/WEB-INF">
		<webflow:flow-location-pattern value="/**/*-flow.xml" />
	</webflow:flow-registry>
 
	<!-- Web Flow views   커스터마이징   있도록 확장하여 사용한다. -->
	<webflow:flow-builder-services id="flowBuilderServices"
		view-factory-creator="mvcViewFactoryCreator" conversion-service="conversionService"
		development="true" />
 
 
	<!-- Web Flow 에서  tiles  사용할  있도록 설정한다. -->
	<bean id="mvcViewFactoryCreator"
		class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator">
		<property name="viewResolvers" ref="tilesViewResolver" />
	</bean>
 
</beans>

상세 : Web Flow views 에 커스터마이징 할 수 있도록 확장하여 사용한다

tiles

URL : http://localhost:8080/swfHelloWorld 으로 처음 접근할 때 index.html 파일이 열리게 된다.

index.html

<html>
  <head>
    <meta http-equiv="Refresh" content="0; URL=spring/start">
  </head>
</html>

위에서 보는 것처럼 “spring/start” URL을 호출한다.
spring/start 에 해당하는 화면은 먼저 설정된 tiles 설정 정보에서 찾는다.

<tiles-definitions>
	<definition name="start" extends="standardLayout">
		<put-attribute name="body" value="/WEB-INF/main.jsp" />
	</definition>
</tiles-definitions>

tiles 관련된 것은 http://tiles.apache.org/를 참조하시길 바랍니다.
등록된 tiles 설정 파일은 앞 설정에서 나왔다. 다시 보면

...
	<!-- tiles 설정 정보를 정의한다. -->
	<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles2.TilesConfigurer">
		<property name="definitions">
			<list>
				<value>/WEB-INF/layouts/layouts.xml</value>
				<value>/WEB-INF/views.xml</value>
				<value>/WEB-INF/sample/views.xml</value>
				<value>/WEB-INF/sample/hello/views.xml</value>
			</list>
		</property>
	</bean> 
...

Hello, Web Flow

다시 돌아와서 Hello , Web Flow 를 화면에 찍어 보도록 하겠다.
Web Flow로 해당 화면의 흐름을 작성한 예를 보자.

hello-flow.xml

<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/webflow 
	http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
 
	<view-state id="hello">
		<transition on="say" to="helloworld" />
	</view-state>
 
	<view-state id="helloworld">
		<transition on="return" to="return" />
	</view-state>
 
	<end-state id="return"	view="externalRedirect:servletRelative:/start" />
 
</flow>

자세한 설명은 flow 정의에서 다루고 있다.
간단하게 보면 view-state, end-state로 나눠져 있는 것을 볼 수 있다. 처음으로 존재하는 view-state는 시작점이라고 생각해도 무방하다. 또한 문자 그대로 end-state는 마지막점이다. hello 화면이 맨 처음 나오고 거기서 helloworld 화면이 보이고 다음은 return이라는 마지막 실행을 하는 것이다.
view-state 안쪽의 transition는 화면에서 클릭하여 이동하게 하는 버튼의 실행이라고 할 수 있다. 여기선 say를 눌러서 실행하면 helloworld 라는 view-state로 이동하는 것이다. 마찬가지로 return을 누르면 externalRedirect:servletRelative:/start으로 이동하는 것이다.

view-state에서 별도의 view를 정의하지 않은 경우 id를 가지고 view를 가져오게 된다. 여기서는 hello 라는 id가 곧 view 명이 되게 된다.
default는 flow.xml과 같은 디렉토리에 있는 화면소스(JSP, xhtml, 등)을 찾게 된다. 여기선 tiles로 정의된 부분을 참조한다.
…/hello/views.xml 파일 내용을 살펴보면,

...
	<definition name="hello" extends="standardLayout">
		<put-attribute name="body" value="/WEB-INF/sample/hello/hello.jsp" />
	</definition> 
...

로 화면에 해당되는 hello.jsp 를 가져오는 것을 확인할 수 있다.

그렇다면 transition은 화면에서 발생한 이벤트와 매핑을 할까? hello.jsp 소스를 잠시 보겠다.

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>Welcome to Spring Web Flow</title>
</head>
<body>
<h1>Welcome to Spring Web Flow</h1>
<form:form id="start">
	<input type="submit" name="_eventId_say" value="Click to say hello!" />
</form:form>
</body>
</html>

hello1-1page.jpg

보는 바와 같이 form으로 둘러싸인 곳에 해답은 있다. <input type=“submit” name=“_eventId_say” …. /> 에서 name 을 보면 _eventId_say로 답을 찾을 수 있다.
_eventId가 답이다. say는 transition의 on과 같음을 확인할 수 있다. eventId 에 정의된 특정 위치의 문자열을 가지고 transition을 분석하다. transition에 대한 내용은 flow 정의에서 자세히 살펴보길 바란다. eventId 가 “say”를 가지고 form 이 전달되면 flow 정의 flow 정의에 따라 transition을 찾고 그에 맞는 state로 넘어가게 된다.
결과는 별로의 값을 가지고 보여주는 화면은 아니고 단지 아래와 같은 화면을 보여주도록 되어 있다.

jhello1-2page.jpg

다음은 입력값이 있는 예를 살펴 보도록 하겠다.

Hello, Web Flow with input value

먼저 flow 정의 파일인 hello2-flow.xml 을 보도록 하자. 실행 시나리오는 on-start ⇒ view-state ⇒ action-state ⇒ decision-stat ⇒ end-state 이다.

hello2-flow.xml

<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/webflow 
	http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
 
 
	<on-start>
		<evaluate expression="helloService.sayMessage()" result="flowScope.message" />
	</on-start>
 
	<view-state id="hello2" model="message">
		<binder>
			<binding property="str" required="true" />
		</binder>
		<transition on="proceed" to="actionHello" />
		<transition on="return" to="return" />
	</view-state>
 
	<action-state id="actionHello">
		<evaluate expression="helloService.addHello(message)" />
		<transition on="yes" to="moreDecision" />
		<transition on="no" to="hello" />
	</action-state>
 
	<decision-state id="moreDecision">
		<if test="helloService.getDecision(message)" then="helloworld2" else="return" />
	</decision-state>
 
	<view-state id="helloworld2">
		<transition on="return" to="return" />
	</view-state>
 
 
	<end-state id="return"	view="externalRedirect:servletRelative:/start" />
</flow>
</xml>

보여 주고자 하는 것은 hello2 화면(view-state)에서 입력 데이터를 객체에 바인딩하고, helloService 서비스 객체를 통해 addHello 메소드 실행, 그 후 결과에 따라 분기문(decision-state)을 통과하여 helloworld2 화면으로 가는 것이다.
간략하게 설명하면, on-start는 flow를 처음 실행할 때 선행하여 실행된다. 여기서는 helloService의 sayMessage를 실행하여 flowScope 내의 message 객체로 저장한다.

HelloService .java

...
@Service("helloService")
public class HelloService implements Iservice {
 
	public Message sayMessage() {
		return new Message();
	}
	...
 
}

flow가 시작할 때 첫 번째로 만나는 view-state는 시작점으로 인식한다. 따라서 view-state “hello2”는 시작점에 해당한다.
hello2.jsp 를 화면에 보여주는데 앞단의 예제와 같다. Spring MVC의 tiles를 이용하여 보여주게 된다.

views.xml

...
	<definition name="hello2" extends="standardLayout">
		<put-attribute name="body" value="inhello2.body" />
	</definition>
 
	<definition name="inhello2.body" template="/WEB-INF/sample/hello2/main.jsp">
		<put-attribute name="helloSection" value="/WEB-INF/sample/hello2/hello.jsp" />
	</definition> 
...

hello.jsp

...
<form:form method="post" >
	<p>
       to Who : <input type="text" id="str" name="str" value=" World ~*"/>
				<script type="text/javascript">
					Spring.addDecoration(new Spring.ElementDecoration({
						elementId : "str",
						widgetType : "dijit.form.ValidationTextBox",
						widgetAttrs : { promptMessage : "for who ? ", required : true }}));
				</script>
				<br>
	</p>
 
  <input id="proceed" type="submit" class="button" 
      name="_eventId_proceed" value="say">
      <script type="text/javascript">
      Spring.addDecoration(new Spring.ValidateAllDecoration({elementId:'proceed', event:'onclick'}));
      </script>
  <input type="submit" class="button" 
      name="_eventId_return" value="index"> 
 
</form:form>
...

상단의 화면은 아래 hello2-flow.xml 내의 view-state와 매핑된다.
여기서 봐야 할 부분은 화면 내의 str 이름의 input 데이터를 message라는 객체로 바인딩하는 부분인다.

...
	<view-state id="hello2" model="message">
		<binder>
			<binding property="str" required="true" />
		</binder>
 
		<transition on="proceed" to="actionHello" />
		<transition on="return" to="return" />
	</view-state>
...

이벤트에 해당하는 proceed 버튼을 클리하면 다음 state로 이동하게 된다.

...
<transition on="proceed" to="actionHello" /> 
...

actionHello은 아래와 같다. 하는 기능은 helloService 객체의 addHello 메소드 호출이다.

...
	<action-state id="actionHello">
		<evaluate expression="helloService.addHello(message)" />
		<transition on="yes" to="moreDecision" />
		<transition on="no" to="hello" />
	</action-state>
...

addHello 메소드는 아래와 같다. 반환되는 값이 boolean인 것을 주목할 필요가 있다. 리턴되는 boolean 값은 transition의 yes, no 와 매핑된다.

...
	public boolean addHello(Message msg){
		try{
		msg.setStr("Hello,"+msg.getStr());
		}catch (Exception e) {
			return false;
		}
		return true;
	}
...

다음 나오는 decision-state는 아래와 같이 분기문의 기능을 수행한다.

...
	<decision-state id="moreDecision">
		<if test="helloService.getDecision(message)" then="helloworld2" else="return" />
	</decision-state>
...

helloworld2 화면으로 이동하게 되면 아래와 같은 jsp 소스를 확인할 수 있다. message 객체의 str 값을 EL 을 이용하여 ${message.str} 으로 보여주고 있다.

<%@ taglib prefix="form"   
    uri="http://www.springframework.org/tags/form" %>
 
<h2>Hello Message?</h2>
<form:form>
  <b>Step two :</b> 
	<fieldset>
		<div class="field">
			<div class="label">
				<label>${message.str}</label>
			</div>		
 
		</div>
		<div class="buttonGroup">
			  <input type="submit" class="button"  name="_eventId_return" value="index"> 
		</div>	
 
    </fieldset>
</form:form>

화면을 다시 보면

hello1-1page.jpg

say 버튼을 누르면,

hello1-1page.jpg

Hello , 뒤에 넣었던 문장이 붙어서 나오게 된다.

4.5 -

4.6 - Spring Web Flow 환경 설정

Spring Web Flow를 사용하기 위해서는 Flow 정의를 위한 XML 문서를 작성하고, FlowRegistry와 FlowExecutor를 설정해야 한다. FlowRegistry는 flow XML을 가져오는 역할을, FlowExecutor는 이를 실행하는 역할을 담당한다.

Spring Web Flow 환경 구성하기

Spring Web Flow를 사용하기 위한 Web 개발 환경에 대한 세팅을 설명한다.

설정

Spring Web Flow의 Flow 정의를 위한 XML 문서는 아래와 같은 Schema를 갖는다.

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:webflow="http://www.springframework.org/schema/webflow-config"
	xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/webflow-config
http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.0.xsd">
 
	<!-- Setup Web Flow here -->
</beans>

기본적인 설정

Spring Web Flow를 사용하려면 FlowRegistry FlowExecutor를 설정해야 한다.

FlowRegistry는 등록될 시나리오에 따라 작성된 flow xml 을 가져오는 역할[1]을 수행한다. FlowExecutor는 등록된 flow 설정 xml을 실행[2]한다. 차후 Spring MVC 와 결합하여 Web Flow 시스템이 실행되는 부분에 대해 다루겠다.

<!-- [1] Flow 설정 파일 등록-->
<webflow:flow-registry id="flowRegistry">
  <webflow:flow-location path="/WEB-INF/flows/booking/booking.xml" />
</webflow:flow-registry>
 
<!-- [2] Flow 실행의 중추 역할을 하는 서비스 제공-->
<webflow:flow-executor id="flowExecutor" />

flow-registry 옵션

flow-registry는 아래 보는 것과 같이 설정할 수 있다.

<!-- [1]기본은 이름-flow.xml이지만, 직접 지정할 수도 있다. -->
<webflow:flow-location path="/WEB-INF/flows/booking/booking.xml" />
 
<!-- [2]id로 식별이 가능하도록 할 수도 있다 -->
<webflow:flow-location path="/WEB-INF/flows/booking/booking.xml" id="bookHotel" />
 
<!-- [3]메타 정보도 등록할 수 있다. -->
<webflow:flow-location path="/WEB-INF/flows/booking/booking.xml">
	<flow-definition-attributes>
		<attribute name="caption" value="Books a hotel" />
	</flow-definition-attributes>
</webflow:flow-location>
 
<!-- [4]ANT 패턴을 지정할 수도 있다. -->
<webflow:flow-location-pattern value="/WEB-INF/flows/**/*-flow.xml" />
 
<!--
	[5]기본 앞 첨자 경로를 지정해서 위치를 조합해서 사용할 수도 있다. 
	    Flow 정의 파일은 모듈화를 높이기 위해서 관련 있는 폴더에 각각 위치해 있는게 가장 좋다.	-->
<webflow:flow-registry id="flowRegistry" base-path="/WEB-INF">
	<webflow:flow-location path="/hotels/booking/booking.xml" />
</webflow:flow-registry>
 
<!-- [6]Flow 상속 구조 구성 가능 -->
<!-- my-system-config.xml -->
<webflow:flow-registry id="flowRegistry" parent="sharedFlowRegistry">
	<webflow:flow-location path="/WEB-INF/flows/booking/booking.xml" />
</webflow:flow-registry>
 
<!-- shared-config.xml -->
<webflow:flow-registry id="sharedFlowRegistry">
	<!-- Global flows shared by several applications -->
</webflow:flow-registry>

커스텀 FlowBuilder 서비스 설정

flow-registry는 아래 보는 것과 같이 설정할 수 있다.

flow-registry에서 flow-builder-services는 flow를 구축하는 데 사용되는 서비스나 설정 등을 커스터마이징할 수 있다. 지정하지 않는 경우에는 기본 서비스가 사용된다.

<webflow:flow-registry id="flowRegistry" 	flow-builder-services="flowBuilderServices">
	<webflow:flow-location path="/WEB-INF/flows/booking/booking.xml" />
</webflow:flow-registry>
 
<webflow:flow-builder-services id="flowBuilderServices" />

구성 가능한 서비스

conversion-service

SWF 시스템에서 사용하는 ConversionService를 커스터마이징. Flow 실행 동안에 필요한 경우 특정 타입을 다른 타입으로 변환해 줌(propertyEditor 성격)

expression-parser

ExpressionParser 커스터마이징하는데 사용. 기본은 Unified EL이 사용되며, 사용하는 영역은 classpath 이다. 다른 ExpressionParser로는 OGNL이 사용 됨.

view-factory-creator

ViewFactoryCreator를 커스터마이징. 디폴트 ViewFactoryCreator는 JSP, Velocity, Freemaker 등을 화면에 보여주게 해주는 Spring MVC ViewFactories로 되어 있음.

development

Flow 개발 모드 설정. true인 경우, Flow 정의가 변경되면 hot-reloading 적용(message bundles 과 같은 리소스 포함).

별도로 커스터마이징 된경우
<webflow:flow-builder-services id="flowBuilderServices"
	conversion-service="conversionService" 
	expression-parser="expressionParser"
	view-factory-creator="viewFactoryCreator" />           
 
<bean id="conversionService" class="..." />
<bean id="expressionParser" class="..." />
<bean id="viewFactoryCreator" class="..." />

flow-executor 옵션

Flow 실행 리스너 붙이기

Flow 실행의 Lifecycle에 관련된 리스너(listener)는 flow-execution-listeners를 이용하여 등록한다.

<flow-execution-listeners>
	<listener ref="securityListener" />
	<listener ref="persistenceListener" />
</flow-execution-listeners>

또한 특정 흐름에 대해서만 적용 가능하다.

<listener ref="securityListener" criteria="securedFlow1,securedFlow2"/>

FlowExecution persistence 조정

<flow-execution-repository max-executions="5" max-execution-snapshots="30" />
max-executions

사용자 세션 당 생성될 수 있는 Flow 실행 개수 지정

max-execution-snapshots

Flow 실행 당 받을 수 있는 이력 snapshot 개수 지정. snapshot을 사용하지 못하게 하려면, 0으로 지정. 제한이 없게 하려면 -1로 설정.

참고자료

4.7 - Spring Web Flow와 Spring MVC 연동

Spring Web Flow는 Spring MVC와 연동해 웹 애플리케이션을 개발할 수 있으며, 이를 위해 web.xml에 DispatcherServlet 설정이 필요하다. DispatcherServlet은 웹 애플리케이션별로 등록되며, 요청 경로와 초기화 파라미터(contextConfigLocation)를 설정한다.

Spring Web Flow 와 MVC 연동

Spring Web Flow를 사용하여 웹을 개발할 때 Spring MVC와 연동하여 개발할 수 있다. 이를 위해 Spring MVC 연동 모듈 등을 설정해야 한다. 여기서는 booking-mvc sample( 실행데모(faces이지만 시나리오는 같음) )을 기준으로 설정하겠다.

설명

Spring MVC 와의 연동을 위해 우리는 web.xml 안에 있는 DispatcherServlet 설정을 보도록 하겠다.

web.xml 환경 구성

Spring MVC를 구성하는 첫 단계는 web.xml에 DispatcherServlet을 구성하는 것이다. DispatcherServlet은 웹 애플리케이션별 하나를 등록한다.

이 예제에서는 /spring/으로 시작하는 모든 요청을 받도록 설정하고 있다. init-param을 사용해 contextConfigLocation을 설정하고 있다.

web.xml

<servlet>
	<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<init-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/web-application-config.xml</param-value>
	</init-param>
</servlet>
<servlet-mapping>
	<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
	<url-pattern>/spring/*</url-pattern>
</servlet-mapping>

Flow로 전달

DispatcherServlet은 애플리케이션 자원에 대한 요청과 핸들러를 매핑시켜 준다. Flow도 핸들러의 하나의 유형으로 처리된다.

FlowHandlerAdapter 등록 및 Flow 매핑 정의

먼저 FlowHandlerAdapter Bean을 정의하고 나서 property(flowExecutor)로 flowExecutor 빈을 설정함으로써 Spring MVC 내에서 Flow를 제어할 수 있도록 한다.

<!-- Enables FlowHandler URL mapping -->
<bean class="org.springframework.webflow.mvc.servlet.FlowHandlerAdapter">
	<property name="flowExecutor" ref="flowExecutor" />
</bean>

이 설정은 Dispatcher가 애플리케이션 자원 경로를 flow registry에 등록된 Flow로 매핑할 수 있도록 해준다. 예를 들어, /hotels/booking 요청은 hotels/booking 이란 Flow ID를 갖는 Flow에게 요청이 가게 된다. flow registry에서 해당 ID 못 찾게 되면, Dispatcher의 순서에 따라 다음 핸들러 매핑에서 찾고, 없다면 “noHandlerFound”(?) 응답이 반환되게 된다.

작업 흐름을 제어하는 Flow

조건에 맞는 flow가 매핑되면 FlowHandlerAdapter는 새로운 flow 실행을 시작할 것인지 아니면 HTTP 요청에 담겨있는 정보를 기반으로 기존 실행을 계속할 것인지를 판단하게 된다.

  • HTTP 요청 파라미터는 모든 Flow 실행의 입력 맵에서 사용할 수 있다.
  • Flow 실행이 마지막 응답 전송 없이 끝나는 경우, Default 핸들러가 동일한 요청을 새로이 Flow 실행을 시도(?)하게 된다
  • 발생한 Excpetion이 NoSuchFlowExecutionException인 경우에는 새로이 Flow 실행을 시작해 복구 시도를 해보며, 다른 예외는 제어하지 않는다.

자세한 내용은 SWF JavaDoc 에서 FlowHandlerAdapter 클래스를 참고하길 바란다.

커스텀 FlowHandler 구현

FlowHandler는 HTTP 서블릿 환경에서 Flow가 실행을 커스터마이징할 수 있는 확장 영역이다. FlowHandlerAdapter에 의해 실행/사용되며, 아래와 같은 수행을 한다.

  • 실행되는 Flow의 id 반환
  • Flow 실행시 입력될 값 생성
  • Flow 실행이 종료되면서 반환하는 결과 처리
  • Flow 실행에서 발생해서 던저진 예외 처리

이러한 수행을 위한 메소드는 org.springframework.mvc.servlet.FlowHandler 인터페이스 형태로 되어 있다.

public interface FlowHandler {
 
	public String getFlowId();
 
	public MutableAttributeMap createExecutionInputMap(HttpServletRequest request);
 
	public String handleExecutionOutcome(FlowExecutionOutcome outcome, HttpServletRequest request, HttpServletResponse response);
 
	public String handleException(FlowException e,	HttpServletRequest request, HttpServletResponse response);
}

FlowHandler를 구현 시, AbstractFlowHandler를 상속하면 된다. 모든 연산은 선택적이 되어서, 필요할 때만 구현하면 되며, 구현하지 않으면 기본 구현 내용(AbstractFlowHandler 내에 구현된)이 적용된다. 특히 다음과 같은 경우에는 구현을 고려할 수 있다.

  • getFlowId(HttpServletRequest) 재정의: HTTP 요청에서 직접적으로 Flow id를 받을 수 없을 때. 일반적으로는 요청의 URI에서 경로 정보를 얻게 됨. 예를 들어, http://localhost/app/hotels/booking?hotelId=1 는 hotels/booking이란 Flow id로 매핑 됨.
  • createExecutionInputMap(HttpServletRequest) 재정의: HttpServletRequest에서 Flow 입력 파라미터를 세부적으로 직접 추출해야 하는 경우. 기본적으로는 모든 요청 파라미터가 Flow 입력 파라미터로 넘겨짐.
  • handleExecutionOutcome 재정의: 직접 Flow 실행 결과를 제어할 필요가 있을 경우. 기본 행동은 Flow의 새로운 실행을 재시작하려고 마지막 Flow의 URL로 redirect 보냄.
  • handleExeception 재정의: 제어되지 못한 Flow 실행을 세심하게 조정할 필요가 있는 경우. 기본적으로는 제어하지 않은 Exception은 Spring MVC ExceptionResolver로 다시 보내진다.

FlowHandler 예제

가장 일반적인 스프링 MVC와의 상호 작용은, Flow가 종료됐을 때 @Controller로 재전송하는 방법이다. FlowHandler는 이를 특정 controller URL을 Flow 정의와 관련 없이 가능하도록 해준다. 예를 보자.

public class BookingFlowHandler extends AbstractFlowHandler {
	public String handleExecutionOutcome(FlowExecutionOutcome outcome, HttpServletRequest request,
	                                     HttpServletResponse response) {
		if (outcome.getId().equals("bookingConfirmed")) {
			return "/booking/show?bookingId=" + outcome.getOutput().get("bookingId");
		} else {
			return "/hotels/index";
		}
	}
}

재정의된 handleExecutionOutcome 메소드에서는 Flow 결과로 나온 flow id 가 bookingConfirmed인 경우 특정 URL(/booking/show?bookingId=…)로 보낸다. flow id 가 bookingConfirmed 와 다른 경우 /hotels/index 에 대한되는 URL로 가게 된다.

커스텀 FlowHandler 배포

커스텀 FlowHandler를 설치하려면,그냥 빈으로 등록하기만 하면 된다. 빈 이름은 반드시 적용하고자 하는 Flow의 id와 일치해야 한다

<bean name="hotels/booking" class="org.springframework.webflow.samples.booking.BookingFlowHandler"/>

이 설정을 통해서 /hotels/booking 자원에 대한 접근은 커스텀 핸들러인 BookingFlowHandler를 사용해서 hotels/booking이 실행되게 된다. booking flow 가 끝나는 시점에서 BookingFlowHandler의 handleExecutionOutcome이 실행되며 String(URL) 결과에 따라 적당한 controller로 재전송된다.

FlowHandler 재전송

FlowExecutionOutcome이나 FlowException을 제어하는 FlowHandler는 제어를 한 후에 재전송하는 경로를 지정하는 String을 반환하게 된다. 이전 예에서 BookingFlowHandler는 bookingConfirmed 결과에 대해서는 booking/show로 재전송하고, 다른 결과에 대해서는 hotels/index 자원 URI로 반환하게 된다. 기본적으로 반환되는 자원의 위치는 현재 서블릿 매핑에 관계된다. 이는 flow handler가 상대 경로를 사용해서 애플리케이션 내에 있는 다른 컨트롤러로 redirect할 수 있게 해준다. 여기에 더해서 좀더 제어가 필요한 경우에 사용할 수 있는 명시적인 앞첨자를 제공한다.

  • servletRelative: 현재 서블릿에 대한 상대적인 자원으로 재전송
  • contextRelative: 현재 웹 애플리케이션 컨텍스트 경로(web application context path)에 대한 상대적인 자원으로 재전송
  • servletRelative: 서블릿 루트에 대한 상대적인 자원으로 재전송
  • http: 또는 https: : 완전한 자원 URI로 재전송

이 앞첨자는 externalRedirect: 지시어와 함께 Flow 정의 내에서도 사용할 수 있다. 예를 들면, view=“externalRedirect:http://springframework.org”.

뷰 결정

Web Flow 2는 따로 지정하지 않는다면 Flow 파일이 있는 디렉터리에 있는 파일과 선택된 뷰 식별자를 매핑해주게 된다. 기존 스프링 MVC+Web Flow 애플리케이션에서는 이미 외부 ViewResolver가 매핑 처리를 해주고 있다. 그러므로 기존 resolver를 계속 사용하고, 기존 Flow 뷰가 패키징된 방법이 변경되는 것을 피하기 위해서 다음처럼 설정하자.

<webflow:flow-registry id="flowRegistry"	flow-builder-services="flowBuilderServices">
	<webflow:location path="/WEB-INF/hotels/booking/booking.xml" />
</webflow:flow-registry>
 
<webflow:flow-builder-services id="flowBuilderServices" 	view-factorycreator="mvcViewFactoryCreator" />
 
<bean id="mvcViewFactoryCreator" class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator">
	<property name="viewResolvers" ref="myExistingViewResolverToUseForFlows" />
</bean>

MvcViewFactoryCreator는 당신이 Spring MVC view 시스템(여기선 “myExistingViewResolverToUseForFlows”)을 Spring Web Flow 내에서 사용할 수 있도록 해준다. Booking Hotels 샘플에서는 아래와 같이 설정되어 있다(tilesViewResolver 를 이용할 수 있도록 되어 있다).

<bean id="mvcViewFactoryCreator" class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator"> 
	<property name="viewResolvers" ref="tilesViewResolver"/> 
</bean>

또한 useSpringBinding의 값을 true로 설정하면 Spring MVC의 고유한 BeanWrapper를 이용하여 데이터 바인딩을 할 수 있다.

뷰에서 이벤트 보내기

flow가 view-state에 들어가서 잠시 멈추게 되면, 사용자를 해당 실행 URL로 재전송해서, 사용자 이벤트가 다시 시작되기를 기다리게 된다. 이번 절에서는 JSP, Velocity나 Freemarker처럼 템플릿 엔진에 의해서 생성된 HTML 기반 뷰에서 이벤트를 발생시키는 방법을 알아보자.

HTMl 버튼을 사용해서 이벤트 보내기

다음은 proceed와 cancle 이벤트를 발생시키는 같은 폼 내의 두 개 버튼을 보여준다.

<input type="submit" name="_eventId_proceed" value="Proceed" />
<input type="submit" name="_eventId_cancel" value="Cancel" />

버튼이 선택되면, SWF는 _eventId로 시작하는 요청 파라미터를 찾아서, 그 부분을 잘라내고 남은 문자열을 id로 사용하게 된다. _eventId_proceed는 proceed가 된다. 그러기 때문에 동일한 폼에서 다양한 여러 이벤트를 발생시킬 수 있다.

hidden HTML 폼 파라미터 사용해 이벤트 신호 보내기

form이 submit될 때 proceed 이벤트가 발생하려면 다음처럼 하자.

<input type="submit" value="Proceed" />
<input type="hidden" name="_eventId" value="proceed" />

여기서는 _eventId 파라미터로 오는 값을 찾아서 event id로 해당 값을 사용하게 된다. 이런 방법은 form으로 전송할 수 있는 이벤트가 하나일 때만 생각해봐야 한다.

<a href="${flowExecutionUrl}&_eventId=cancel">Cancel</a>

매개변수 식별 순서는 “eventId” ⇒ “_eventId” ⇒ 없음 이다.

참고 자료

4.8 - Spring Web Flow에서 보안 적용

Spring Security를 통해 Flow 실행에 보안을 적용하려면 인증 및 권한 규칙을 설정하고, Flow 정의에 secured 구성요소를 추가하며, SecurityFlowExecutionListener를 사용하여 보안 규칙을 처리한다. 보안 규칙은 Flow, state, transition 단계에서 적용 가능하다.

Flow에 보안 적용하기(Securing Flows)

개요

보안은 어플리케이션 에서 매우 중요한 이슈이다. Spring Security는 어플리케이션과 결합하여 여러 수준에서 보안을 책임지는 플랫폼의 기능을 수행한다. 여기서는 Web Flow에 적용되는 Spring Security에 대해 알아보겠다.

어떻게 Flow를 안전하게 할 수 있을까?

Flow 실행에 보안을 적용시키고 싶다면 다음 단계에 따르자.

  1. Spring Security에서 인증(authentication)과 권한(authorization) 규칙을 설정한다.
  2. secured 구성요소로 Flow 정의에 보안 규칙을 추가한다.
  3. 보안 규칙을 처리해주는 SecurityFlowExecutionListener 추가한다.

secured 구성요소

secured 구성 요소는 접근하기 전에 권한 확인을 적용해 주며, Flow 실행 단계마다 한 번 이상은 나올 수 없다. Flow 실행에서 세 단계로 보안을 적용할 수 있다. Flow, state, transition에 보안 적용이 가능하다. 사용되는 문법은 동일하다. secured 구성요소는 보안이 적용되어야 하는 구성 요소 내에 위치하면 된다. 예를 들어 view state에 보안을 적용하고자 하면,

<view-state id="secured-view">
 <secured attributes="ROLE_USER" />
 ...
</view-state>

속성에 보안 적용

attributes 속성은 ‘,’(콤마)로 구분해서 SS의 권한 속성을 리스트로 입력할 수 있다. 이 속성은 대부분 허가된 보안 롤(role)을 명시하게 된다. 스프링 시큐리티 접근 결정 매니저(access decision manager)에 의해 이 속성에 입력한 값과 사용자가 가지고 있는 값을 비교한다.

<secured attributes="ROLE_USER" />

기본적으로, 롤 기반 접근 결정 관리자를 사용하여 사용자가 접근할 수 있는지 확인한다. 만약 애플리케이션이 권한 룰을 사용하지 않는다면 이 부분을 오버라이딩할 필요가 있다.

타입 맞춰보기

두 가지 유형의 일치 유형 제공: any, all.

<secured attributes="ROLE_USER, ROLE_ANONYMOUS" match="any" />

이 속성은 필수가 아니다. 정의하지 않으면 기본 값은 any다.

SecurityFlowExecutionListener

Web Flow 설정에 추가한다. SecurityFlowExecutionListener가 Web Flow 설정에 정의되어 있어야 플로우 실행기(executor)에 적용된다.

<webflow:flow-executor id="flowExecutor"
	flow-registry="flowRegistry">
	<webflow:flow-execution-listeners>
		<webflow:listener ref="securityFlowExecutionListener" />
	</webflow:flow-execution-listeners>
</webflow:flow-executor>
<bean id="securityFlowExecutionListener"
	class="org.springframework.webflow.security.SecurityFlowExecutionListener" />

보안 설정에 의해서 접근이 거절되면, AccessDeniedException이 발생한다. 기본으로 롤 기반 의사결정이 이루어 지지만, 커스텀 의사결정 관리자를 지정할 수 있다.

<bean id="securityFlowExecutionListener" 	class="org.springframework.webflow.security.SecurityFlowExecutionListener">
	<property name="accessDecisionManager" ref="myCustomAccessDecisionManager" />
</bean>

Spring Security 환경 구성

스프링 환경 구성

http와 authentication-provider로 정의하면 된다.

<security:http auto-config="true">
	<security:form-login login-page="/spring/login"
		login-processing-url="/spring/loginProcess" default-target-url="/spring/main"
		authentication-failure-url="/spring/login?login_error=1" />
	<security:logout logout-url="/spring/logout" logout-success-url="/spring/logout-success" />
</security:http>
 
<security:authentication-provider>
	<security:password-encoder hash="md5" />
	<security:user-service>
		<security:user name="keith" password="417c7382b16c395bc25b5da1398cf076"	authorities="ROLE_USER,ROLE_SUPERVISOR" />
		<security:user name="erwin" password="12430911a8af075c6f41c6976af22b09"	authorities="ROLE_USER,ROLE_SUPERVISOR" />
		<security:user name="jeremy" password="57c6cbff0d421449be820763f03139eb" authorities="ROLE_USER" />
		<security:user name="scott" password="942f2339bf50796de535a384f0d1af3e"	authorities="ROLE_USER" />
	</security:user-service>
</security:authentication-provider>

web.xml 환경 구성

필터 설정.(SS 기본)

<filter>
	<filter-name>springSecurityFilterChain</filter-name>
	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
	<filter-name>springSecurityFilterChain</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

참고자료

4.9 - Flow가 관리하는 영속성(Flow Managed Persistence)

Web Flow는 Flow 실행 중에 PersistenceContext를 생성하고, Flow가 종료될 때 데이터를 commit하여 영속성 관리를 지원하며, 주로 하이버네이트와 JPA와 연동된다. 이 패턴은 동시에 수정되는 데이터를 보호하기 위해 optimistic locking과 함께 사용되며, HTTP 세션 기반 저장 방법과는 달리 Flow 범위 내에서 영속성을 관리한다.

Flow가 관리하는 영속성(Flow Managed Persistence)

개요

대부분의 애플리케이션은 여러 방법으로 데이터에 접근한다. 여러 사용자가 공유하는 데이터를 동시에 수정한다. 따라서 트랜잭션 데이터 접근 속성이 필요하다. 관계형 데이터 집합을 도메인 객체로 변환하여 애플리케이션 처리를 도와준다. Web Flow는 “Flow가 관리하는 영속성”(flow managed persistence)을 제공하여 Flow가 객체 영속성 문맥을 만들고, commit 하고, 닫을 수 있도록 한다. Web Flow는 하이버네이트와 JPA 객체 영속화 기술과 연동한다.

Flow-관리 영속성과 별도로 PesistenceContext 관리를 애플리케이션의 서비스 계층에서 완전히 캡슐화하는 패턴이 있다. 이런 경우 Web 계층은 영속화에 관여하지 않는다. 그 대신 서비스 계층으로 념주겨거나 반환받은 detached object를 가지고 동작한다. 이번 장은 Flow-관리 영속성에 초점을 맞추고 이 기능을 언제 어떻게 사용하는지 살펴보겠다.

설명

Flow 범위(FlowScoped) PersistenceContext

이 패턴은 Flow 시작 시에 flowScope으로 PersistenceContext를 생성해 준다. 이 persistence context는 Flow 실행 과정 동안에 데이터 접근을 하는데 사용하며, Flow가 종료될 때 persistent entity에서 변경된 내용을 반영(commit) 한다. 이 패턴은 대부분 다중 사용자에 의해서 동시에 수정되는 데이터의 정합성을 보호하고자 optimistic locking 전략과 함께 사용된다.

저장이나 재시작 능력이 필요 없다면, Flow 상태를 표준 HTTP 세션 기반 저장 방법이 충분하다. 이 때 커밋 전 세션 만료나 종료(termination)는 잠재적으로 변경 사항을 손실할 수 있다. FlowScoped PersistenceContext 패턴을 사용하려면 먼저 persistence-context로 Flow를 식별하게 해야 한다.

<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
	<persistence-context />
</flow>

그 다음으로 이 패턴을 적용할 Flow에 적절한 FlowExecutionListener를 설정하자. 하이버네이트를 사용한다면, HibernateFlowExecutionListener를 등록하고, JPA를 사용한다면, JpaFlowExecutionListener를 등록하자.

<webflow:flow-executor id="flowExecutor"
		flow-registry="flowRegistry">
	<webflow:flow-execution-listeners>
		<webflow:listener ref="jpaFlowExecutionListener" />
	</webflow:flow-execution-listeners>
</webflow:flow-executor>
 
<bean id="jpaFlowExecutionListener" class="org.springframework.webflow.persistence.JpaFlowExecutionListener">
  <constructor-arg ref="entityManagerFactory" />
  <constructor-arg ref="transactionManager" />
</bean>

Flow가 종료되는 시점에 커밋이 일어나게 하려면, end-state의 commit 속성을 입력하자.

<end-state id="bookingConfirmed" commit="true" />

이걸로 끝이다.

이제 Flow가 시작할 때 리스너가 flowScope에 새로운 EntityManager를 할당해서 제어하게 된다. Flow 내에서 스프링 기반 데이터 접근 객체를 사용해서 발생하는 데이터 접근 시에는 항상 이 EntityManager를 자동으로 사용하게 된다. 이러한 데이터 접근 연산은 중간 수정 내용의 고립성 유지를 위해 항상 트랜잭션 처리 대상이 되지 않고, 읽기 전용 트랜잭션에서만 실행되어야 한다.

참고자료

4.10 -

4.11 - Flow 정의

Flow는 여러 단계의 흐름을 캡슐화한 재사용 가능한 구조로, 상태(state)로 구성되며 각 상태는 이벤트에 따라 다른 뷰로 전환된다. 웹 애플리케이션 개발자는 XML 기반의 Flow 정의 언어로 Flow를 작성하며, 첫 번째 상태가 Flow의 시작점이 된다.

Flow 정의

개요

Flow

Flow란 상이한 상황(context)에서 실행될 수 있는 재사용이 가능한 여러 단계들의 흐름을 캡슐화한 것을 의미한다. 모든 Flow는 아래와 같은 Root로 시작한다.

<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/webflow
    http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
 
</flow>

Flow의 구성

SWF에서 Flow는 “Sate(state)“로 부르는 일련의 단계들로 구성된다. Flow로 진입하게 되는 Sate는 일반적으로 사용자에게 보여지는 뷰가 된다. 이 뷰에서는 Sate를 제어하게 되는 이벤트가 발생한다. 이들 이벤트는 결과적으로 다른 뷰로 이동하게 되는 Transition(transition)을 일으키게 된다. 모든 state는 <flow/> 안에 정의하게 된다. 맨 처음 정의되는 state가 Flow의 시작점이 된다.

Flow의 작성법

Flow는 웹 애플리케이션 개발자가 XML 기반 Flow 정의 언어를 사용해서 작성된다.

설명

Flow의 필수적인 언어 구성요소

<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/webflow
                     http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
 
	<view-state id="enterBookingDetails" />
 
	<view-state id="enterBookingDetails">
		<transition on="submit" to="reviewBooking" />
	</view-state>
 
	<end-state id="bookingCancelled" />
 
</flow>
  • view-state: Flow 중 화면을 보여주는 Sate를 정의하는 구성 요소
  • 편의상 Flow 정의 파일이 있는 디렉터리 내에서 view-state id와 일치하는 화면 템플릿을 맞춰 보게 됨
  • transition: Sate 내에서 발생한 이벤트를 제어하는 구성 요소. 화면 이동을 일으킴.
  • end-state: Flow의 결과를 정의

Actions

대부분의 Flow는 화면 이동 로직 뿐만 아니라, 애플리케이션의 비즈니스 서비스나 다른 행동을 호출할 필요가 있을 수 있다. Flow 내에서 Action을 취할 수 있는 여러 지점이 존재한다.

  • Flow가 시작할 때
  • Sate에 들어갈 때
  • 화면을 보여줄 때
  • Transition이 일어날 때
  • Sate가 종료될 때
  • Flow가 종료될 때

SWF에서 Action은 기본적으로 Unified EL이라는 간결한 표현 언어를 사용해서 정의하게 된다.

evaluate

대부분 evaluate 구성 요소를 사용하게 된다. 이를 통해 Spring Bean에 있는 메소드나 다른 Flow 변수를 호출할 수 있다. 예를 들면 아래와 같다.

<!-- [1] entityManager Bean 의 persist 메소드에 booking 객체를 넣어 호출한다.  -->
<evaluate expression="entityManager.persist(booking)" />
 
<!-- [2] findHotels 메소드 호출하고 실행결과 Hotels 객체를 flowScope 데이타 모델에  저장한다. -->
<evaluate expression="bookingService.findHotels(searchCriteria)" result="flowScope.hotels" />
 
<!-- [3] findHotels 메소드 호출하고 실행결과 Hotels 객체를 flowScope 데이타 모델에  저장시 dataModel 타입으로 변환하여 저장한다.  -->
<evaluate expression="bookingService.findHotels(searchCriteria)" result="flowScope.hotels" result-type="dataModel"/>

아래 예에서는 Flow가 시작할 때 Flow 범위에 Booking 객체를 생성해 저장한다. hotelId는 Flow의 입력 속성으로 받게 된다.

<flow xmlns="http://www.springframework.org/schema/webflow"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/webflow
    http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
 
	<input name="hotelId" />
 
	<on-start>
		<evaluate expression="bookingService.createBooking(hotelId, currentUser.name)"	result="flowScope.booking" />
	</on-start>
 
	<view-state id="enterBookingDetails">
		<transition on="submit" to="reviewBooking" />
	</view-state>
 
	<view-state id="reviewBooking">
		<transition on="confirm" to="bookingConfirmed" />
		<transition on="revise" to="enterBookingDetails" />
		<transition on="cancel" to="bookingCancelled" />
	</view-state>
 
	<end-state id="bookingConfirmed" />
	<end-state id="bookingCancelled" />
</flow>

입력/출력 매핑

각각의 Flow는 잘 정의된 입력/출력 계약(input/output contract)를 갖고 있다. Flow는 시작할 때 입력 속성을 건네받게 되고, 종료될 때 출력 속성을 반환하게 된다. 이처럼 Flow 호출은 개념적으로 다음과 같은 메소드 호출과 비슷하다.

FlowOutcome flowId(Map<String, Object> inputAttributes);

반환되는 FlowOutcome은 다음과 같은 메소드 선언부를 갖게 된다.

public interface FlowOutcome {
	public String getName();
	public Map<String, Object> getOutputAttributes();
}
입력
<!-- [1] 해당 변수의 값은 flow scope 내에 hotelId 이란 이름으로 저장된다.  -->
<input name="hotelId" />
 
<!-- [2] type 속성으로 속성 지정 가능. 타입이 일치하지 않다면 타입 변환 시도 -->
<input name="hotelId" type="long" />
 
<!-- [3] value 속성으로 입력 값을 할당 -->
<input name="hotelId" value="flowScope.myParameterObject.hotelId" />
 
<!-- [4] required 속성으로 null이나 비어있지 못하도록 강제 -->
<input name="hotelId" type="long" value="flowScope.hotelId" required="true" />
출력

Flow 출력 속성은 output 구성 요소를 사용한다. output 속성은 end-state 내에 선언한다. 출력 값은 속성의 이름으로 Flow 범위 내에서 얻어오게 된다.

<end-state id="bookingConfirmed">
  <output name="bookingId" />
</end-state>
 
<!-- 직접 대상 값 지정 -->
<end-state id="bookingConfirmed">
  <output name="confirmationNumber" value="booking.confirmationNumber" />
</end-state>
입력/출력 매핑:샘플
<flow xmlns="http://www.springframework.org/schema/webflow"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/webflow
                      http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
	<input name="hotelId" />
 
	<on-start>
		<evaluate expression="bookingService.createBooking(hotelId, currentUser.name)" 	result="flowScope.booking" />
	</on-start>
 
	<view-state id="enterBookingDetails">
		<transition on="submit" to="reviewBooking" />
	</view-state>
 
	<view-state id="reviewBooking">
		<transition on="confirm" to="bookingConfirmed" />
		<transition on="revise" to="enterBookingDetails" />
		<transition on="cancel" to="bookingCancelled" />
	</view-state>
 
	<end-state id="bookingConfirmed">
		<output name="bookingId" value="booking.id" />
	</end-state>
	<end-state id="bookingCancelled" />
</flow>

위 Flow는 이제 hotelId를 입력 값으로 받아서, 새로운 예약이 끝나게 되면 bookingId 출력 속성을 결과로 반환하게 된다.

변수들

Flow에는 하나 이상의 인스턴스 변수 선언이 가능하다. 이 변수들은 flow가 시작할 때 할당되며, 변수를 유지하게 되는 모든 @Autowired transient 참조는 Flow가 재시작될 때 다시 값이 할당(rewired) 되게 된다. var 구성 요소를 사용해서 Flow 변수를 선언하자.

<var name="searchCriteria" class="com.mycompany.myapp.hotels.search.SearchCriteria"/>

변수로 사용하는 클래스가 Flow 요청 간 인스턴스의 Sate를 유지하기 위해서 java.io.Serializable을 interface로 가지고 있어야 함을 기억하자.

Sub Flow 호출

Flow 내에서 하위 Flow로써 또 다른 Flow 호출이 가능하다. 이때 하위 Flow가 결과를 반환할 때까지 기존 Flow는 대기하게 된다.

subflow-state

subflow-state 구성요소를 사용해서 하위 Flow 호출을 하게 된다.

<subflow-state id="addGuest" subflow="createGuest">
	<transition on="guestCreated" to="reviewBooking">
		<evaluate expression="booking.guests.add(currentEvent.attributes.guest)" />
	</transition>
	<transition on="creationCancelled" to="reviewBooking" />
</subfow-state>

이 예제에서는 createGuest Flow를 호출한다. guestCreated 출력이 반환되면, 새로운 손님이 예약 손님 리스트에 추가된다.

subflow input 전달

input 구성요소를 사용하면 하위 Flow에 입력값을 건낼 수 있다.

<subflow-state id="addGuest" subflow="createGuest">
	<input name="booking" />
	<transition to="reviewBooking" />
</subfow-state>
subflow output 매핑

출력 값의 이름으로 하위 Flow에서 출력하는 속성을 참조해서 Transition을 하게 된다.

<subflow-state  ..>
  <transition on="guestCreated" to="reviewBooking">
    <evaluate expression="booking.guests.add(currentEvent.attributes.guest)" />
  </transition>
  ..

이 예에서는 guestCreated을 반환하게 될 때 gest 이름으로 넘어온 값을 booking 내의 guests (currentEvent.attributes.guest)의 일부로 추가해주고 있다.

샘플:Sub Flow 호출하기

아래는 샘플 코드이다.

<flow xmlns="http://www.springframework.org/schema/webflow"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/webflow
                      http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
 
	<input name="hotelId" />
	<on-start>
		<evaluate expression="bookingService.createBooking(hotelId, currentUser.name)"
			result="flowScope.booking" />
	</on-start>
 
	<view-state id="enterBookingDetails">
		<transition on="submit" to="reviewBooking" />
	</view-state>
 
	<view-state id="reviewBooking">
 
		<transition on="addGuest" to="addGuest" />
		<transition on="confirm" to="bookingConfirmed" />
		<transition on="revise" to="enterBookingDetails" />
		<transition on="cancel" to="bookingCancelled" />
	</view-state>
 
	<subflow-state id="addGuest" subflow="createGuest">
		<transition on="guestCreated" to="reviewBooking">
			<evaluate expression="booking.guests.add(currentEvent.attributes.guest)" />
		</transition>
		<transition on="creationCancelled" to="reviewBooking" />
	</subfow-state>
 
	<end-state id="bookingConfirmed">
		<output name="bookingId" value="booking.id" />
	</end-state>
	<end-state id="bookingCancelled" />
</flow>

참고자료

4.12 - Web Flow에서 Expression Language (EL) 사용

Web Flow는 데이터 모델 및 action 실행을 위해 Unified EL 또는 OGNL을 사용하며, 주로 클라이언트 데이터 접근, 내부 데이터 구조 접근, 스프링 빈 메소드 호출 등에서 EL을 활용한다. Flow에서 EL 표현식은 eval 표현과 템플릿 표현을 통해 데이터 구조에 접근하거나 동적 뷰를 생성할 수 있다.

Expression Language

개요

Web Flow는 데이터 모델 및 action 실행을 위해 EL을 이용한다. 우리는 EL에 대해 알아보면서 flow 정의를 해보도록 하겠다.

설명

지원하는 EL 구현체

Unified EL

기본으로는 Unified EL을 사용한다. jboss-el이 기본 구현체로 되어 있다.

참고 : web 컨테이너에서는 대게 el-api 를 지원해준다. 톰캣 6의 경우처럼 말이다.

OGNL

OGNL은 SWF2에서 제공하는 또 다른 EL이다. 클래스패스에만 추가하면 자동으로 찾아서 사용한다.

EL 호환성

Unified EL과 OGNL은 비슷한 문법을 가지고 있다. 가능하면 Unified EL만 사용하자.

EL 사용법

Flow에서 EL 사용하는 경우

  • 클라이언트에서 제공되는 데이터에 접근하는 경우. 입력 속성이나 요청 파라미터
  • flowScope처럼 내부 데이터 구조에 접근하는 경우
  • 스프링 빈에 있는 메소드 호출
  • 생성자 결정할 때

Flow에 의해서 보여지는 뷰는 EL을 사용해서 Flow 데이터 구조에 접근하게 됨

표현 타입

표준 eval 표현

가장 일반적인 방법은 eval 표현으로, 이 경우 ${}나 #{}을 사용하면 안 된다. 이 예는 searchCriteria에 있는 nextPage() 호출.

<evaluate expression="searchCriteria.nextPage()" />
표현 템플릿

다음은 “template” 표현식으로 아래와 같은 형태로 ${} 을 사용할 수 있다.

<view-state id="error" view="error-${externalContext.locale}.xhtml" />

externalContext 에 세팅되어 있는 locale 결과를 대체하여 error-결과.xhtml 로 생성된

특별한 EL 변수

Scope
  • flowScope: flow 변수에 할당되며, Flow 범위를 가진 객체. 기본적으로 Flow 범위에 저장되는 모든 객체는 Serializable 이어야 함.
<evaluate expression="searchService.findHotel(hotelId)" result="flowScope.hotel" />
  • viewScope: view 변수에 할당되며, view-state 내 범위를 갖음. 그러므로 view-state 내에서만 참조 가능. 역시 모든 객체는 Serializable 이어야 함.
<on-render>
  <evaluate expression="searchService.findHotels(searchCriteria)" result="viewScope.hotels" result-type="dataModel" />
</on-render>
  • requestScope: request 변수에 할당. 한 번의 Flow 내에서 공유
<set name="requestScope.hotelId" value="requestParameters.id" type="long" />
  • flashScope: flash 변수에 할당. Flow가 시작될 때 할당되고, 뷰가 보여지고 난 후 clear 됐다가, Flow가 종료되면 정리되는 범위. 객체는 Serializable 해야 함.
<set name="flashScope.statusMessage" value="'Booking confirmed'" />
  • conversationScope

convesation 변수에 할당. 최상위 Flow가 시작할 때 할당되며, 최상위 Flow가 종료될 때 정리. 최상위 Flow의 자식 Flow에서 공유. HTTP session에 저장되며, 세션 복제를 할 경우를 대비해 Serizalizable를 구현해야 함.

<evaluate expression="searchService.findHotel(hotelId)" result="conversationScope.hotel"/>
context
  • flowRequestcontext: 현재 Flow 요청을 표현. RequestContext API.
  • messageContext: 에러나 성공 메세지를 포함해서 Flow 실행 메세지를 받아오고, 만드는데 대한 context에 접근할 수 있음. MessageContext 참조.
<evaluate expression="bookingValidator.validate(booking, messageContext)" />
  • flowExecutionContext: 현재 Flow 상태를 표현. FlowExecutionContext API.
  • externalContext: 사용자 세션 속성 등 외부 환경에 접근할 수 있음. ExternalContext API.
<evaluate expression="searchService.suggestHotels(externalContext.sessionMap.userProfile)" result="viewScope.hotels" />
그 외
  • requestParameters: 사용자로부터 넘어온 request 매개변수 접근
<set name="requestScope.hotelId" value="requestParameters.id" type="long" />
  • currentEvent: 현재 Event 객체에 접근
<evaluate expression="booking.guests.add(currentEvent.guest)" />
  • currentUser: 인증된 Principal에 접근
<evaluate expression="bookingService.createBooking(hotelId, currentUser.name)" result="flowScope.booking" />
  • resourceBundle: message 자원 관리
<set name="flashScope.successMessage" value="resourceBundle.successMessage" />
  • flowExecutionUrl: 현재 flow execution view-state에 대한 context-relative URI에 접근

범위 검색 알고리즘

특정 범위에 변수를 할당할 때는 반드시 범위를 명시해야 한다.

<set name="requestScope.hotelId" value="requestParameters.id" type="long" />

특정 범위에 있는 변수에 접근할 때는 꼭 범위를 명시할 필요는 없다.

<evaluate expression="entityManager.persist(booking)" />

booking처럼 범위를 명시하지 않은 경우, 범위 검색 알고리즘(scope searching algorithm)이 동작하며, 이 알고리즘은 request→flash→view→flow→conversation 범위의 순서로 찾게 된다. 없을 경우 EvaluationException 발생. 아래그림은 검색되는 Scope 순서를 잘 보여주고 있다.

scopsofswf

참고자료

  • Spring Web Flow reference 2.0.x (링크 만료됨)
  • Spring Web-Flow Framework Reference beta with Korean (by 박찬욱)
  • Pro Spring 2.5(Apress) - Chapter 18 Spring Web Flow

4.13 - Spring Web Flow의 뷰(View) 보여주기

view-state는 화면을 생성해 보여준 후, 사용자의 응답을 기다리는 역할을 하며, ID가 별도의 view 설정 없이 곧 뷰를 의미한다. 예를 들어, “enterBookingDetails"라는 ID는 해당 뷰를 나타내는 역할을 한다.

뷰(View) 보여주기

개요

view-state는 flow 내에서 화면을 생성하는 요소이다. 여기서는 view-state 에 대해서 알아보도록 하자.

설명

뷰 상태(view state) 정의하기

view-state는 기본적으로 해당 뷰를 생성하여 보여준 후, 사용자가 화면을 통해 응답을 하는 것을 기다린다. 아래는 view-state는 enterBookingDetails라는 ID 를 가지고 있으며 또한 별도의 view 설정이 없기 때문에 ID 가 곧 view를 뜻한다

<view-state id="enterBookingDetails">
	<transition on="submit" to="reviewBooking" />
</view-state>

따라서. 디렉토리 상의

dir

booking.xml(or booking-flow.xml) 이 존재하는 디렉토리에 있는 enterBookingDetails.jsp 이 자동으로 view로 동작한다. 또는 절대 경로를 이용하여 명시적으로 view=”/WEB-INF/hotels/booking/enterBookingDetails.jsp” 설정할 수도 있다. 아래에서 다시 설명하겠다.

뷰 식별자 지정하기

뷰 속성을 여러 방법으로 지정할 수 있다.

  • 상대 경로 사용
<view-state id="enterBookingDetails" view="bookingDetails.xhtml">
  • 절대 경로 사용
<view-state id="enterBookingDetails" view="/WEB-INF/hotels/booking/bookingDetails.jsp>
  • 논리적인 경로 사용: Spring MVC 등과 통합 시
<view-state id="enterBookingDetails" view="bookingDetails">

뷰 범위

view-state 내부에서 유지되는 변수. Ajax 요청처럼 동일한 뷰를 여러 번 보여줘야 하는 경우 유용.

  • var 태그를 사용해서 view 변수 선언.
<var name="searchCriteria" class="com.mycompany.myapp.hotels.SearchCriteria" />
  • viewScope 변수에 할당하기
<on-render>
	<evaluate expression="bookingService.findHotels(searchCriteria)" result="viewScope.hotels"/>
</on-render>

뷰 범위내에서 Object 다루기

아래 코드는 화면 ID가 searchResults인 view-state 화면을 그리되 그리기 전 bookingService.findHotels(searchCriteria) 메소드를 호출한 후 그 결과를 viewScope내의 hotels로 저장한 후 화면을 보여주고 있다. 그리고 화면상에 next 또는 previous 이벤트 발생시 eval(searchCriteria.nextPage()/previousPage()) 이 발생 그 결과를 fragments으로 지정된 영역에 뿌려주고 있다.

<view-state id="searchResults">
	<on-render>
		<evaluate expression="bookingService.findHotels(searchCriteria)"
			result="viewScope.hotels" />
	</on-render>
	<transition on="next">
		<evaluate expression="searchCriteria.nextPage()" />
		<render fragments="searchResultsFragment" />
	</transition>
	<transition on="previous">
		<evaluate expression="searchCriteria.previousPage()" />
		<render fragments="searchResultsFragment" />
	</transition>
</view-state>

자세한 것은 아래에서 다시 설명하도록 하겠다.

화면을 보여줄 때 액션 실행

뷰를 보여주기 전에 특정 액션을 실행하려면 on-render를 사용한다.

<on-render>
  <evaluate expression="bookingService.findHotels(searchCriteria)" result="viewScope.hotels" />
</on-render>

Model Binding

<view-state id="enterBookingDetails" model="booking">

뷰 이벤트가 발생했을 때 지정된 모델에 대해서 다음 행동이 일어난다.

  1. view-to-model binding.
  2. 모델 유효성 검증.

타입 변환 수행

변환기(Converter) 구현

org.springframework.binding.convert.converters.TwoWayConverter을 구현하면 된다. StringToObject를 구현하는게 더 좋다.

protected abstract Object toObject(String string, Class targetClass) throws Exception;
protected abstract String toString(Object object) throws Exception;

구현 예.

public class StringToMonetaryAmount extends StringToObject {
	public StringToMonetaryAmount() {
	super(MonetaryAmount.class);
  }
  @Override
  protected Object toObject(String string, Class targetClass) {
  	return MonetaryAmount.valueOf(string);
  }
  @Override
  protected String toString(Object object) {
  	MonetaryAmount amount = (MonetaryAmount) object;
  	return amount.toString();
  }
}

org.springframework.binding.convert.converters에 이미 구현된 변환기가 위치.

변환기 등록하기

org.springframework.binding.convert.service.DefaultConversionService을 상속해서 addDefaultConverters() 메소드를 재정의 하면 된다. 자세한 것은 시스템 설정에서 ConversionService 확장을 이용하여 설정하는 곳에서 다루고 있다.

바인딩 금지하기

bind 속성으로 특정 뷰 이벤트에서 모델 바인딩과 유효성 검증을 안 하게 할 수도 있다.

<view-state id="enterBookingDetails" model="booking">
	<transition on="proceed" to="reviewBooking">
	<transition on="cancel" to="bookingCancelled" bind="false" />
</view-state>

명시적으로 바인딩 지정하기

아래와 같이 binder 속성으로 바인딩 할 프로퍼티를 명시적으로 지정할 수 있다.

<view-state id="enterBookingDetails" model="booking">
	<binder>
		<binding property="creditCard" />
		<binding property="creditCardName" />
		<binding property="creditCardExpiryMonth" />
		<binding property="creditCardExpiryYear" />
	</binder>
	<transition on="proceed" to="reviewBooking" />
	<transition on="cancel" to="cancel" bind="false" />
</view-state>

binder로 지정하지 않으면 모든 프로퍼티를 바인딩된다. converter를 이용하여 변환기 지정이 가능하다.

<view-state id="enterBookingDetails" model="booking">
	<binder>
		<binding property="checkinDate" converter="shortDate" />
		<binding property="checkoutDate" converter="shortDate" />
		<binding property="creditCard" />
		<binding property="creditCardName" />
		<binding property="creditCardExpiryMonth" />
		<binding property="creditCardExpiryYear" />
	</binder>
	<transition on="proceed" to="reviewBooking" />
	<transition on="cancel" to="cancel" bind="false" />
</view-state>

Model 유효성 검증

Model 유효성 검사에 대한 부분은 Web flow에서는 프로그래밍적으로 제약사항을 강제화하는 형태로 지원하고 있다.

프로그램 내에서 유효성 검증

첫 번째 방법으로 유효성 검증 로직을 모델 객체 내에 정의하는 방법이다. Web Flow 는 view-stat에서 모델로 넘어간 시점(view-state postback lifecycle)에서 자동적으로 validate 메소드를 자동으로 호출한다.

<view-state id="enterBookingDetails" model="booking">
	<transition on="proceed" to="reviewBooking">
</view-state>

Booking class 내의 validate(view-state 명) 코드는 아래와 같이 볼 수 있다.(메소드명 : validate + EnterBookingDetails)

public class Booking {
	private Date checkinDate;
	private Date checkoutDate;
  ...
	public void validateEnterBookingDetails(ValidationContext context) {
		MessageContext messages = context.getMessages();
		if (checkinDate.before(today())) {
			messages.addMessage(new MessageBuilder().error().source("checkinDate").
 
			defaultText("Check in date must be a future date").build());
		} else if (!checkinDate.before(checkoutDate)) {
			messages.addMessage(new MessageBuilder().error()
			                                        .source("checkoutDate")
			                                        .defaultText("Check out date must be later than check in date")
			                                        .build());
		}
	}
}

enterBookingDetails에 대한 이벤트가 발생했을 때 자동으로 validateEnterBookingDetails이 호출 된다. 메소드 이름을 validate$ 로 정의하면 된다.

Validator 구현

Validator로 불리는 별도의 객체로 정의할 수도 있다. 클래스 이름을 $Validator 로 지정하면 된다. 메소드 이름은 역시 validate$로 한다. 아래 클래스명은 Booking + Validator 이며 메소드 이름은 validate + EnterBookingDetails 임을 볼 수 있다.

@Component
public class BookingValidator {
	public void validateEnterBookingDetails(Booking booking, ValidationContext context) {
		MessageContext messages = context.getMessages();
		if (booking.getCheckinDate().before(today())) {
			messages.addMessage(new MessageBuilder().error()
			                                        .source("checkinDate")
			                                        .defaultText("Check in date must be a future date")
			                                        .build());
		} else if (!booking.getCheckinDate().before(booking.getCheckoutDate())) {
			messages.addMessage(new MessageBuilder().error()
			                                        .source("checkoutDate")
			                                        .defaultText("Check out date must be later than check in date")
			                                        .build());
		}
	}
}

spring mvc의 Error 객체도 받을 수 있다.

ValidationContext

유효성 검증 동안에 MessageContext에 접근할 수 있게 해주며, 다양한 객체에 접근 가능하게 해준다.

유효성 검증 하지 않기

validate=“false”로 설정하면 유효성 검사를 하지 않을 수 있다.

<view-state id="chooseAmenities" model="booking">
	<transition on="proceed" to="reviewBooking">
	<transition on="back" to="enterBookingDetails" validate="false" />
</view-state>

뷰 transition 실행

전이 대상은 (1)다른 뷰, (2)현재 뷰를 다시, (3)action을 실행, (4)Ajax 이벤트를 제어할 때 ‘fragments’로 불리는 일부 뷰를 보여주라는 요청일 수도 있다.

전이 액션(Transition actions)

<transition on="submit" to="bookingConfirmed">
	<evaluate expression="bookingAction.makeBooking(booking, messageContext)" />
</transition>
public class BookingAction {
	public boolean makeBooking(Booking booking, MessageContext context) {
		try {
			bookingService.make(booking);
			return true;
		} catch (RoomNotAvailableException e) {
			context.addMessage(builder.error().defaultText("No room is available at this hotel").build());
			return false;
		}
	}
}

글로벌 전이(Global transitions)

<global-transitions>
	<transition on="login" to="login">
	<transition on="logout" to="logout">
</global-transitions>

이벤트 핸들러(Event handlers)

<transition on="event">
	<!-- Handle event -->
</transition>

프레그먼트 보여주기(fragments)

현재 뷰 중 일부만을 다시 보여줄 수 있는 방법으로, Ajax 기반일 때 주로 사용한다.

<transition on="next">
	<evaluate expression="searchCriteria.nextPage()" />
	<render fragments="searchResultsFragment" />
</transition>

‘,‘로 구분해서 다수의 fragment를 지정할 수도 있다.

메세지 사용하기

MessageContext는 플로우 실행 동안에 메세지를 저장하는 데 사용되는 API다. 일반 메세지나 국제화가 지원된 메세지 모두 사용 가능하다. 메세지 수준도 지정 가능하며, 지원되는 수준은 info, warning, error이 있다. 메세지를 추가할 때는 MessageBuilder를 사용하자.

  • 일반 메세지 추가
MessageContext context = ...
MessageBuilder builder = new MessageBuilder();
context.addMessage(builder.error().source("checkinDate").defaultText("Check in date must be a future date").build());
context.addMessage(builder.warn().source("smoking").defaultText("Smoking is bad for your health").build());
context.addMessage(builder.info().defaultText("We have processed your reservation - thank you and enjoy your stay").build());
  • 국제화가 지원되는 메세지 추가
MessageContext context = ...
MessageBuilder builder = new MessageBuilder();
context.addMessage(builder.error().source("checkinDate").code("checkinDate.notFuture").build());
context.addMessage(builder.warn().source("smoking").code("notHealthy").resolvableArg("smoking").build());

메세지 번들 사용하기

스프링의 MessageSource를 사용해서 메세지 번들 정의가 가능하다. 간단히 프로퍼티 파일로 관리하면 된다.

#messages.properties
checkinDate=Check in date must be a future date
notHealthy={0} is bad for your health
reservationConfirmation=We have processed your reservation - thank you and enjoy your stay

뷰나 플로우에서는 resourceBundle EL 변수로 접근도 가능하다.

<h:outputText value="#{resourceBundle.reservationConfirmation}" />

시스템 생성 메세지 이해하기

시스템에서 발생한 예외에 대해 메세지 지정할 수 있다. 예를 들어 타입 변환 시 예외가 발생하면 typeMismatch를 통해서 메세지 지정할 수 있다.

booking.checkinDate.typeMismatch=The check in date must be in the format yyyy-mm-dd.

팝업 띄우기

모달 팝업 다이얼로그를 뷰로 렌더링하고 싶다면, view-state 내에 popup=“true”로 설정하면 된다.

<view-state id="changeSearchCriteria" view="enterSearchCriteria.xhtml" popup="true">

특히 스프링 자바 스크립트와 함께 사용하면, 팝업을 보여주는데 클라이언트 코드가 전혀 필요 없다. SWF가 클라이언트 요청을 팝업으로 재전송(redirect)해준다.

뷰 백트랙킹(View backtracking)

기본적으로 브라우저의 백 버튼으로 이전 view-state로 돌아갈 수 있다. history를 사용해서 이에 대한 설정이 가능하다.

  • ‘discard’로 설정하면 백트랙킹(backtracking)을 방지할 수 있다.
<transition on="cancel" to="bookingCancelled" history="discard">
  • ‘invalidate’로 설정하면 이전에 보여줬던 모든 뷰뿐만 아니라 현재 뷰까지도 백트랙킹을 방지한다.
<transition on="confirm" to="bookingConfirmed" history="invalidate">

참고자료

4.14 - Action 실행

Action-state는 flow 내에서 특정 액션 실행 후 그 결과에 따라 다른 상태로 전이하는 기능을 제공하며, decision-state는 if-else와 같은 흐름 제어를 수행한다. 예를 들어, 특정 조건에 따라 transition이 결정되는 구조를 정의할 수 있다.

Action 실행

개요

action-state는 flow 내에서 action 실행을 제어하기 위한 요소이다.
decision-state를 이용하여 if-else와 같은 흐름 제어를 할 수 있다. 좀 더 자세히 알아보도록 하자.

설명

액션 상태 정의하기

특정 액션을 호출한 다음에, 그 결과에 따라서 다른 상태로 전이하고 싶은 경우에는 action-state 구성 요소를 사용하자.
직관적으로 봤을 때 아래 코드는 interview.moreAnswersNeeded()의 결과값에 의해 transition이 실행될 것을 예상할 수 있다.

<action-state id="moreAnswersNeeded">
	<evaluate expression="interview.moreAnswersNeeded()" />
	<transition on="yes" to="answerQuestions" />
	<transition on="no" to="finish" />
</action-state>

좀더 완전한 예를 살펴보자.

<flow xmlns="http://www.springframework.org/schema/webflow"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/webflow
                      http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
	<on-start>
		<evaluate expression="interviewFactory.createInterview()"
			result="flowScope.interview" />
	</on-start>
 
	<view-state id="answerQuestions" model="questionSet">
		<on-entry>
			<evaluate expression="interview.getNextQuestionSet()"
				result="viewScope.questionSet" />
		</on-entry>
		<transition on="submitAnswers" to="moreAnswersNeeded">
			<evaluate expression="interview.recordAnswers(questionSet)" />
		</transition>
	</view-state>
 
	<action-state id="moreAnswersNeeded">
		<evaluate expression="interview.moreAnswersNeeded()" />
		<transition on="yes" to="answerQuestions" />
		<transition on="no" to="finish" />
	</action-state>
 
	<end-state id="finish" />
 
</flow>

의사결정 상태(decision states) 정의

action-state를 대신해서 편리하게 if/else 문법을 사용해서 이동하고자 하는 의사결정을 해주는 decision-state를 사용한다.
이전 예제를 의사결정 상태로 구현한 예를 보자.

<decision-state id="moreAnswersNeeded">
	<if test="interview.moreAnswersNeeded()" then="answerQuestions" else="finish" />
</decision-state>  

액션 출력 이벤트 매핑

액션은 대부분 POJO의 메소드를 호출한다. action-state와 decision-state을 호출했을 때, 이들 메소드가 반환하는 값은 상태를 전이하게 해주는데 사용할 수 있다. 전이가 이벤트에 의해서 발생되기 때문에, 우선 메소드가 반환하는 값은 반드시 Event 객체에 매핑되어야 한다.
다음 테이블은 공통적으로 반환하는 값 타입에 따라 Event 객체가 어떻게 매핑되는지를 설명해준다.

메소드 반환 타입 매핑된 Event 식별자 표현

결과로 리턴되는 타입메핑되는 이벤트 값
java.lang.StringString 값
java.lang.Booleanyes(true에 해당), no(false에 해당)
java.lang.Enum EnumEnum 이름
나머지 다른 타입success

예제.moreAnswersNeeded() 메소드의 리턴 타입은 boolean인 것을 예상할 수 있으면 그에 따라 yes, no에 매핑됨을 알 수 있다.

<action-state id="moreAnswersNeeded">
	<evaluate expression="interview.moreAnswersNeeded()" />
	<transition on="yes" to="answerQuestions" />
	<transition on="no" to="finish" />
</action-state>

액션 구현

POJO 로직처럼 action 코드를 작성하는 것이 가장 일반적이다.
때로는 flow context에 접근할 필요가 있는 액션 코드를 작성할 필요가 있다.
이럴 때는 POJO를 호출하면서, EL 변수로 flowRequestContext를 건낼 수 있다.
그 대신 Action 인터페이스를 구현하거나, MultiAction 기본 클래스를 상속할 수도 있다.

POJO 메소드 호출

<evaluate expression="pojoAction.method(flowRequestContext)" />
public class PojoAction {
 public String method(RequestContext context) {
  ...
 }
}

custom Action 구현 호출

<evaluate expression="customAction" />
public class CustomAction implements Action {
  public Event execute(RequestContext context) {
  ...
  }
}

MultiAction 구현 호출

<evaluate expression="multiAction.actionMethod1" />
public class CustomMultiAction extends MultiAction {
	public Event actionMethod1(RequestContext context) {
  ...
	}
  ...
	public Event actionMethod2(RequestContext context) {
  ...
	}
 
}

액션 예외

action은 복잡한 비즈니스 로직을 캡슐화하고 있는 서비스를 호출할 수도 있다.
이 서비스들은 비즈니스 예외를 던질 수도 있으니 이를 처리해야 할 수도 있다.

POJO 액션 사용 시 비즈니스 예외 제어하기

<evaluate expression="bookingAction.makeBooking(booking, flowRequestContext)" />
public class BookingAction {
	public String makeBooking(Booking booking, RequestContext context) {
		try {
			BookingConfirmation confirmation = bookingService.make(booking);
			context.getFlowScope().put("confirmation", confirmation);
			return "success";
		} catch (RoomNotAvailableException e) {
 
			context.addMessage(new MessageBuilder().error().efaultText("No room is available at this hotel").build());
 
			return "error";
		}
	}
}

MultiAction 사용 시 비즈니스 예외 제어하기

아래 예제는 이전 예제와 기능적으로는 동일하지만, POJO 액션 대신 MultiAction으로 구현했다.
Event ${methodName}(RequestContext) 규약에 따라 메소드를 구성하면 되고, POJO의 자유스러움에 비해, 보다 더 강력한 타입 안정성을 제공한다.

<evaluate expression="bookingAction.makeBooking" />
public class BookingAction extends MultiAction {
	public Event makeBooking(RequestContext context) {
		try {
			Booking booking = (Booking) context.getFlowScope().get("booking");
			BookingConfirmation confirmation = bookingService.make(booking);
			context.getFlowScope().put("confirmation", confirmation);
			return success();
		} catch (RoomNotAvailableException e) {
			context.getMessageContext()
			       .addMessage(new MessageBuilder().error().defaultText("No room is available at this hotel").build());
			return error();
		}
	}
}

다른 Action 실행 예제

on-start

<flow xmlns="http://www.springframework.org/schema/webflow"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
	<input name="hotelId" />
	<on-start>
		<evaluate expression="bookingService.createBooking(hotelId, currentUser.name)"
			result="flowScope.booking" />
	</on-start>
</flow>

on-entry

<view-state id="changeSearchCriteria" view="enterSearchCriteria.xhtml"
	popup="true">
	<on-entry>
		<render fragments="hotelSearchForm" />
	</on-entry>
</view-state>

on-exit

<view-state id="editOrder">
	<on-entry>
		<evaluate expression="orderService.selectForUpdate(orderId, currentUser)"
			result="viewScope.order" />
	</on-entry>
	<transition on="save" to="finish">
		<evaluate expression="orderService.update(order, currentUser)" />
	</transition>
	<on-exit>
		<evaluate expression="orderService.releaseLock(order, currentUser)" />
	</on-exit>
</view-state>

on-end

<flow xmlns="http://www.springframework.org/schema/webflow"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/webflow
                      http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
	<input name="orderId" />
	<on-start>
		<evaluate expression="orderService.selectForUpdate(orderId, currentUser)"
			result="flowScope.order" />
	</on-start>
	<view-state id="editOrder">
		<transition on="save" to="finish">
			<evaluate expression="orderService.update(order, currentUser)" />
		</transition>
	</view-state>
	<on-end>
		<evaluate expression="orderService.releaseLock(order, currentUser)" />
	</on-end>
</flow>

on-render

<view-state id="reviewHotels">
	<on-render>
		<evaluate expression="bookingService.findHotels(searchCriteria)"
			result="viewScope.hotels" result-type="dataModel" />
	</on-render>
	<transition on="select" to="reviewHotel">
		<set name="flowScope.hotel" value="hotels.selectedRow" />
	</transition>
</view-state>

on-transition

<subflow-state id="addGuest" subflow="createGuest">
	<transition on="guestCreated" to="reviewBooking">
		<evaluate expression="booking.guestList.add(currentEvent.attributes.newGuest)" />
	</transition>
</subfow-state>

Named actions

<action-state id="doTwoThings">
	<evaluate expression="service.thingOne()">
		<attribute name="name" value="thingOne" />
	</evaluate>
	<evaluate expression="service.thingTwo()">
		<attribute name="name" value="thingTwo" />
	</evaluate>
	<transition on="thingTwo.success" to="showResults" />
</action-state>

Streaming actions

아래 예는 flow에서 printBoardingPassAction를 호출하는 것으로 PDF로 프린트하고자 할 때 구현하는 예를 보여주고 있다.
AbstractAction을 상속한 PrintBoardingPassAction의 doExecute() 메소드 안에 실제 pdf 관련 소스를 구현하고 success()를 리턴한다.

<view-state id="reviewItinerary">
	<transition on="print">
		<evaluate expression="printBoardingPassAction" />
	</transition>
</view-state>
public class PrintBoardingPassAction extends AbstractAction {
  public Event doExecute(RequestContext context) {
    // stream PDF content here...
    // - Access HttpServletResponse by calling context.getExternalContext().getNativeResponse();
    // - Mark response complete by calling context.getExternalContext().recordResponseComplete();
    return success();
  }
}

참고자료

4.15 - Flow 상속

Flow 상속은 상위 Flow의 설정을 하위 Flow에서 사용할 수 있도록 하며, 주로 global transition과 예외 핸들러를 상속받는 데 사용된다. 자바 상속과 유사하지만, 하위 Flow는 상위 Flow의 요소를 재정의할 수 없으며, 여러 상위 Flow를 상속받을 수 있다.

Flow 상속

개요

Flow 상속은 한 Flow가 다른 Flow 설정을 상속할 수 있게 되어 있다. 상속은 Flow와 State 레벨에서 모두 발생할 수 있다. 가장 흔한 유즈케이스는 상위 Flow로 global transition과 예외 핸들러를 정의하고 하위 Flow로 그 설정을 상속받는 것이다. 상위 Flow를 찾으려면 다른 Flow들처럼 flow-registry에 추가해야 된다.

설명

Flow 상속은 자바 상속과 비슷한가?

상위에 정의한 요소를 하위에서 접근할 수 있다는 측면에서는 자바 상속과 Flow 상속이 비슷하다. 하지만 몇 가지 차이점을 가지고 있다.

하위 Flow는 상위 Flow의 요소를 재정의할 수 없다. 상위와 하위 Flow에 있는 동일한 요소는 병합된다. 상위 Flow에만 있는 요소는 하위 Flow에 추가된다.

하위 Flow는 여러 상위 Flow를 상속받을 수 있다. 그러나, 자바 상속은 단일 클래스로 제한된다.

Flow 상속 타입

Flow 수준 상속

Flow 수준 상속은 flow 내에 parent 속성을 이용하여 정의한다. 이 속성은 콤마로 구분하여 상속받을 Flow를 표현한다. 하위 flow는 목록에 명시된 순서대로 각각의 상위 Flow를 상속받는다. 첫 번째 상속으로 상위 Flow에 있는 요소와 내용을 추가하고 나면 그것을 다시 하위 Flow로 간주하고 그 다음 상위 flow를 상속 받는다. 아래 예를 보면 common-transitions를 먼저 상속 받고 다음에 common-states를 상속 받는다.

<flow parent="common-transitions, common-states">

State 수준 상속

State 수준 상속은 Flow 수준 상속과 비슷하다. 유일한 차이점은 Flow 전체가 아니라 오직 해당 State 하나만 상위로부터 상속받는다. Flow 상속과 달리 오직 하나의 상위만 허용한다. 또한 상속받을 Flow State의 식별자가 반드시 정의되어 있어야 한다. Flow와 State 식별자는 #로 구분한다. 상위와 하위 State는 반드시 같은 타입이어야 한다. 예를 들어 view-state는 ent-state를 상속받을 수 없다. 오직 view-state만 상속받을 수 있다

<view-state id="child-state" parent="parent-flow#parent-view-state">

추상 Flow

종종 상위 Flow는 직접 호출하지 않도록 설계한다. 그런 Flow를 실행하지 못하도록 abstract로 설정할 수 있다. 만약 추상 Flow를 실행하려고 하면 FlowBuilderException가 발생한다.

<flow abstract="true">

상속 알고리즘

하위 Flow가 상위 Flow를 상속할 때 발생하는 기본적인 일은 상위와 하위 Flow를 병합하여 새로운 Flow를 만드는 것이다. 웹 Flow 정의 언어에는 각각의 엘리먼트에 대해 어떻게 병합할 것인가에 대한 규칙이 있다. 엘리먼트에는 두 종류가 있다. 병합 가능한 것(mergeable)과 병합이 가능하지 않은 것(non-mergeable)이 있다. 병합 가능(mergeable)한 엘리먼트는 만약 엘리먼트가 같다면 병합을 시도한다. 병합이 가능하지 않은(non-mergeable) 엘리먼트는 항상 최종 Flow에 직접 포함된다. 병합 과정 중에 수정하지 않는다.

주의

  • 상위 Flow에 있는 외부 리소스 경로는 절대 경로여야 한다. ** 상대 경로는 두 Flow를 병합할 때 상위 Flow와 하위 Flow가 위치한 디렉토리가 다르면 깨질 수 있다. 일반 병합하면, 상위 Flow에 있던 모든 상대 경로는 하위 Flow 기준으로 바뀐다.

병합 가능한(mergeable) 엘리먼트

만약 같은 타입의 엘리먼트고 입력한 속성이 같다면 상위 엘리먼트의 내용을 하위 엘리먼트로 병합한다. 병합 알고리즘은 계속해서 병합하는 상위와 하위의 서브 엘리먼트를 각각 병합한다. 그렇지 않으면 상위 Flow의 엘리먼트를 하위 Flow에 새로운 엘리먼트로 추가한다.

대부분의 경우 상위 flow의 엘리먼트가 하위 Flow 엘리먼트에 추가된다. 이 규칙에 예외로는 시작할 때 추가될 action 엘리먼트(evaluate, render, set)가 있다. 상위 action의 결과를 하위 action 결과로 사용하게 한다.

병합이 가능한 엘리먼트는 다음과 같다.

  • action-state: id
  • attribute: name
  • decision-state: id
  • end-state: id
  • flow: 항상 병합
  • if: test
  • on-end: 항상 병합
  • on-entry: 항상 병합
  • on-exit: 항상 병합
  • on-render: 항상 병합
  • on-start: 항상 병합
  • input: name
  • output: name
  • secured: attributes
  • subflow-state: id
  • transition: on and on-exception
  • view-state: id

병합할 수 없는 엘리먼트

병합할 수 없는 엘리먼트는 다음과 같다

  • bean-import
  • evaluate
  • exception-handler
  • persistence-context
  • render
  • set
  • var

참고자료

5 - 데이터처리

데이터처리 서비스는 데이터베이스에 대한 연결 및 영속성 처리, 선언적인 트랜잭션 관리를 지원한다.

데이터처리

데이터처리 서비스는 데이터베이스에 대한 연결 및 영속성 처리, 선언적인 트랜잭션 관리를 지원한다.

5.1 - DataSource 서비스

DataSource 서비스는 데이터베이스 연결을 제공하며, 추상화 계층을 통해 업무 로직과 데이터베이스 연결 방식 간의 종속성을 제거한다. 이를 통해 다양한 방식의 데이터베이스 연결을 지원하고 유연성을 제공한다.

DataSource 서비스

개요

데이터베이스에 대한 연결을 제공하는 서비스이다. 다양한 방식의 데이터베이스 연결을 제공하고,이에 대한 추상화계층을 제공함으로써, 업무 로직과 데이터베이스 연결 방식 간의 종속성을 배제한다.

설명

Connection Provider 별 DataSource implementions

Connection Provider별 Connection 객체를 얻기 위한 로직을 구현한 DataSource 구현체를 사용한다.

JDBCDataSource

JDBC driver를 이용하여 Database Connection을 생성한다.

Configuration
<bean id="dataSource"
	class="org.springframework.jdbc.datasource.DriverManagerDataSource">
	<property name="driverClassName" value="${driver}" />
	<property name="url" value="${dburl}" />
	<property name="username" value="${username}" />
	<property name="password" value="${password}" />
</bean>
PROPERTIES설 명
driverClassNameJDBC driver class name설정
urlDatabase에 접근하기 위한 JDBC URL
usernameDatabase 접근하기 위한 사용자명
passwordDatabase 접근하기 위한 암호
Sample Source
@Resource(name = "dataSource")
DataSource dataSource;
 
@Resource(name = "jdbcProperties")
Properties jdbcProperties;
 
boolean isHsql = true;
 
@Test
public void testJdbcDataSource() throws Exception {
 
        assertNotNull(dataSource);
        assertEquals("org.springframework.jdbc.datasource.DriverManagerDataSource", 
                                                        dataSource.getClass().getName());
 
        Connection con = null;
        Statement stmt = null;
        ResultSet rs = null;
 
        try {
            con = dataSource.getConnection();
            assertNotNull(con);
            stmt = con.createStatement();
            rs = stmt.executeQuery("select 'x' as x from dual");
            while (rs.next()) {
                assertEquals("x", rs.getString(1));
            }
           ........
    

DBCPDataSource

JDBC driver를 이용한 Database Connection 구현체이다.Commons DBCP라 불리는 Jakarta의 Database Connection Pool이다.

Configuratioin
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
	<property name="driverClassName" value="${driver}"/>
	<property name="url" value="${dburl}"/>
	<property name="username" value="${username}"/>
	<property name="password" value="${password}"/>
	<property name="defaultAutoCommit" value="false"/>
	<property name="poolPreparedStatements" value="true"/>
</bean>
PROPERTIES설 명
driverClassNamejdbc driver의 class name 설정
urlDatabase url을 설정
usernameDatabase 접근하기 위한 사용자명
passwordDatabase 접근하기 위한 암호
defaultAutoCommitdatasource로부터 리턴된 connection에 대한 auto-commit 여부를 설정
poolPreparedStatementsPreparedStatement 사용여부
initialSizeConnection pool에 생성될 초기 connection size 설정
maxTotal
(1.x에서는 maxActive)
동시에 할당할 수 있는 active connection 최대 갯수를 설정
maxIdlepool에 남겨놓을 수 있는 idle connection 최대 갯수를 설정
minIdle최소한으로 유지할 connection 갯수를 설정
maxWaitMillis
(1.x에서는 maxWait)
모든 Connection이 사용중일 경우 최대 대기 시간을 설정
defaultReadOnlyConnection Pool에 의해 생성된 Connection에 read-only 속성 부여
defaultTransactionIsolation리턴된 connection에 대한 transaction isolation 속성 부여
defaultCatalogConnection의 catlog 설정
testOnBorrowConnection pool에서 객체를 가지고 오기 전에 그 객체의 유효성을 확인할 것인지 결정
testOnReturn객체를 return하기 전에 객체의 유효성을 확인할 것인지 결정
validationQueryvalidationQuery를 설정
loginTimeoutDatabase에 연결하기 위한 login timeout(in seconds)을 설정
timeBetweenEvictionRunsMillis놀고 있는 connection을 pool에서 제거하는 시간기준(기본 -1) 단위 1/1000초
Sample Source
@Resource(name = "dataSource")
DataSource dataSource;
 
@Resource(name = "jdbcProperties")
Properties jdbcProperties;
 
boolean isHsql = true;
 
@Test
public void testDbcpDataSource() throws Exception 
{
 
  assertNotNull(dataSource);
  assertEquals("org.apache.commons.dbcp.BasicDataSource", dataSource.getClass().getName());
 
  Connection con = null;
  Statement stmt = null;
  ResultSet rs = null;
 
  try 
  {
    con = dataSource.getConnection();
    assertNotNull(con);
    stmt = con.createStatement();
    rs = stmt.executeQuery("select 'x' as x from dual");
    while (rs.next()) {
        assertEquals("x", rs.getString(1));
    }
   } catch (Exception e) {
    fail("Jdbc DataSource Test Failed! : " + e.getMessage());
    e.printStackTrace();
   } 
   ........
}

C3P0DataSource

JDBC driver를 이용한 Database Connection를 생성하는 구현체. C3P0 Library에 관련 사항은 C3P0 Configuration에서 확인할 수 있다.

Configuration
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
	destroy-method="close">
	<property name="driverClass" value="${driver}" />
	<property name="jdbcUrl" value="${dburl}" />
	<property name="user" value="${username}" />
	<property name="password" value="${password}" />
	<property name="initialPoolSize" value="3" />
	<property name="minPoolSize" value="3" />
	<property name="maxPoolSize" value="50" />
	<!-- <property name="timeout" value="0" /> -->   <!-- 0 means: no timeout -->
	<property name="idleConnectionTestPeriod" value="200" />
	<property name="acquireIncrement" value="1" />
	<property name="maxStatements" value="0" />  <!-- 0 means: statement caching is turned off.  -->
	<!-- c3p0 is very asynchronous. Slow JDBC operations are generally performed 
                by helper threads that don't hold contended locks. 
		Spreading these operations over multiple threads can significantly improve performance 
		by allowing multiple operations to be performed simultaneously -->
	<property name="numHelperThreads" value="3" />  <!-- 3 is default -->
</bean>
PROPERTIES설 명
driverClassjdbc driver
jdbcUrlDB URL
user사용자명
password패스워드
initialPoolSize풀 초기값
minPoolSize풀 최소값
maxPoolSize풀 최대값
idleConnectionTestPeriodidle상태 점검시간
acquireIncrement증가값
maxStatements캐쉬유지여부
numHelperThreadsHelperThread 개수
Sample Source
@Resource(name = "dataSource")
DataSource dataSource;
 
@Resource(name = "jdbcProperties")
Properties jdbcProperties;
 
@Test
public void testC3p0DataSource() throws Exception 
{
 
        assertNotNull(dataSource);
        assertEquals("com.mchange.v2.c3p0.ComboPooledDataSource", dataSource.getClass().getName());
 
        Connection con = null;
        Statement stmt = null;
        ResultSet rs = null;
 
        try {
            con = dataSource.getConnection();
            assertNotNull(con);
            stmt = con.createStatement();
            rs = stmt.executeQuery("select 'x' as x from dual");
            while (rs.next()) {
                assertEquals("x", rs.getString(1));
            }
        } catch (Exception e) {
            fail("Jdbc DataSource Test Failed! : " + e.getMessage());
            e.printStackTrace();
        }
  ...................
}

JNDIDataSource

JNDIDataSource는 JNDI Lookup을 이용하여 Database Connection을 생성한다. JNDIDataSource는 대부분 Enterprise application server에서 제공되는 JNDI tree로 부터 DataSource를 가져온다.

Configuration

jee tag를 사용하기 위해서는 Spring XML Configuration 파일의 머릿말에 namespace와 schemaLocation를 추가해야 한다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.5.xsd">
 
    <!-- <bean/> definitions here -->
 
</beans>

Jeus 설정

<jee:jndi-lookup id="dataSource" jndi-name="${jndiName}" resource-ref="true">
     <jee:environment>
	  java.naming.factory.initial=${jeus.java.naming.factory.initial}
	  java.naming.provider.url=${jeus.java.naming.provider.url}
    </jee:environment>
</jee:jndi-lookup>

Weblogic 설정

<util:properties id="jndiProperties" location="classpath:/META-INF/spring/jndi.properties" />
<jee:jndi-lookup id="dataSource" jndi-name="${jndiName}" resource-ref="true" environment-ref="jndiProperties" />
PROPERTIES설 명
jndiTemplateJNDI 검색을 위해 JNDI 템플릿을 설정
jndiEnvironmentJNDI를 검색하기 위해 JNDI 환경을 설정
resourceRefJ2EE 컨테이너에서 검색할 수 있는지 설정
expectedTypeJNDI 객체의 타입을 지정
jndiName검색을 위해 JNDI 이름을 설정
proxyInterfaceJNDI 객체를 사용하기 위해 proxy 인터페이스를 설정
lookupOnStartupstarup시에 JNDI object를 검색할 지 여부를 설정
cacheJNDI 객체를 캐싱할 것인지 설정
defaultObjectJNDI lookup에 실패하였을 경우 전달할 default object를 지정
Sample Source
@Resource(name = "dataSource")
DataSource dataSource;
 
@Resource(name = "jdbcProperties")
Properties jdbcProperties;
 
@Test
public void testJndiJeusDataSource() throws Exception 
{
 
        assertNotNull(dataSource);
        assertEquals("jeus.jdbc.connectionpool.DataSourceWrapper", dataSource.getClass().getName());
 
        Connection con = null;
        Statement stmt = null;
        ResultSet rs = null;
 
        try {
            con = dataSource.getConnection();
            assertNotNull(con);
            stmt = con.createStatement();
            rs = stmt.executeQuery("select 'x' as x from dual");
            while (rs.next()) {
                assertEquals("x", rs.getString(1));
            }
        } catch (Exception e) {
            fail("Jdbc DataSource Test Failed! : " + e.getMessage());
            e.printStackTrace();
        } 
        ...................
}
  • Jeus5.0 datasource : jeus.jdbc.driver.oracle.OracleConnectionPool
  • Jeus6.0 datasource : jeus.jdbc.connectionpool.DataSourceWrapper
@Resource(name = "dataSource")
DataSource dataSource;
 
@Resource(name = "jdbcProperties")
Properties jdbcProperties;
 
@Test
public void testJndiDataSource() throws Exception 
{
 
        assertNotNull(dataSource);
        assertEquals("weblogic.jdbc.common.internal.RmiDataSource_922_WLStub", dataSource.getClass().getName());
 
        Connection con = null;
        Statement stmt = null;
        ResultSet rs = null;
 
        try {
            con = dataSource.getConnection();
            assertNotNull(con);
            stmt = con.createStatement();
            rs = stmt.executeQuery("select 'x' as x from dual");
            while (rs.next()) {
                assertEquals("x", rs.getString(1));
            }
        } catch (Exception e) {
            fail("Jdbc DataSource Test Failed! : " + e.getMessage());
            e.printStackTrace();
        } 
        ...................
}
  • Weblogic datasource

참고자료

5.2 - Data Access 서비스

Data Access 서비스는 다양한 데이터베이스 솔루션과 접근 기술에 일관된 방식으로 대응하며, 데이터 조회, 입력, 수정, 삭제 기능을 단순화한다. 또한 데이터베이스와의 접점을 추상화하여 변경 시 시스템 수정 최소화를 지원하고, 템플릿 방식으로 개발 효율을 높인다.

Data Access 서비스

개요

Data Access 서비스는 다양한 데이터베이스 솔루션 및 데이터베이스 접근 기술에 일관된 방식으로 대응하기 위한 서비스로서,데이터를 조회하거나 입력, 수정, 삭제하는 기능을 수행하는 메커니즘을 단순화한다. 또한 데이터베이스 솔루션이나 접근 기술이 변경될 경우에도 데이터를 다루는 시스템 영역의 변경을 최소화할 수 있도록 데이터베이스와의 접점을 추상화하며, 추상화된 데이터 접근 방식을 템플릿(Template)으로 제공함으로써, 개발자들의 업무 효율을 향상시킨다.

iBATIS 프레임워크

전자정부 프레임워크에서는 JDBC 를 사용한 Data Access 를 추상화하여 간편하고 쉽게 사용할 수 있는 Data Mapper framework 인 iBATIS 를 Data Access 기능의 기반 오픈 소스로 채택하였다. iBATIS 를 사용하면 관계형 데이터베이스에 억세스하기 위해 필요한 일련의 자바 코드 사용을 현저히 줄일 수 있으며 간단한 XML 기술을 사용하여 SQL 문을 JavaBeans (또는 Map) 에 간편하게 맵핑할 수 있다.

  • 추상화된 접근 방식 제공 : JDBC 데이터 억세스에 대한 추상화된 접근 방식으로 간편하고 쉬운 API, 자원 연결/해제, 공통 에러 처리 등을 통합 지원한다.
  • 코드로부터 SQL 분리 지원 : 소스코드로부터 SQL 문을 분리하여 별도의 repository(의미있는 문법의 XML)에 유지하고 이에 대한 빠른 참조구조를 내부적으로 구현하여 관리/유지보수/튜닝의 용이성을 보장한다.
  • 쿼리 실행의 입/출력 객체 바인딩/맵핑 지원 : 쿼리문의 입력 파라메터에 대한 바인딩과 실행결과 resultset 의 가공(맵핑) 처리시 객체(VO, Map, List) 수준의 자동화를 지원한다.
  • Dynamic SQL 지원 : 코드 작성, API 직접 사용없이 입력 조건에 따른 동적인 쿼리문 변경을 지원한다.
  • 다양한 DB 처리 지원 : 기본 질의 외에 Batch SQL, Paging, Callable Statement, BLOB/CLOB 등 다양한 DB처리를 지원한다.

세부 사항 설명

iBATIS Data Mapper API 는 XML을 사용하여 SQL 문에 대한 객체 맵핑을 간편하게 기술할 수 있도록 지원하며, 자바빈즈 객체와 Map 구현체, 다양한 원시 래퍼 타입(String, Integer..) 등을 PreparedStatement 의 파라메터나 ResultSet에 대한 결과 객체로 쉽게 맵핑해준다.

image

  • 파라메터 객체를 제공한다. (마찬가지로 자바빈즈, Map 또는 원시 래퍼 일 수 있다.) 파라메터 객체는 update 문, 쿼리의 where 절의 input 변수로 세팅될 것이다.
  • mapped statement 를 실행한다. Data Mapper 프레임워크는 PreparedStatement 의 인스턴스를 생성하여 위에서 제공된 파라메터 객체를 이용해 파라메터를 세팅하고(바인드 변수처리),쿼리문을 실행하고 결과를 ResultSet 으로 부터 결과 객체로 작성한다.
  • update 문의 경우에는 변경 반영된 rows 수를 리턴하고, 조회의 경우 단건 조회 용 single 객체 또는 여러건 조회를 위한 Collection (객체의 List) 을 리턴하게 된다. 파라메터 객체와 마찬가지로 결과 객체의 JavaBean 이나 Map, primitive type wrapper 또는 XML 문서가 될 수 있다.

설명

Data Access 서비스에 대한 자세한 설명에 앞서 간단하게 Data Access 서비스를 시작하는데 필요한 것에 대한 설명을 하고자 한다.

Step1. 사전 준비

필요 Library

본 서비스를 활용하기 위해서 필요한 Library 목록과 설명은 아래와 같다.

라이브러리설 명연관 라이브러리
ibatis-sqlmap-2.3.4.726.jariBATIS 라이브러리(필수)
commons-dbcp-1.2.2.jardatabase connection pooling 지원 라이브러리(선택)
commons-logging-1.1.1.jarcommons 로깅(선택)
log4j-1.3alpha-8.jarlog4j(선택)
oscache-2.4.jar중앙집중 또는 분산 캐슁 지원(선택)
cglib-nodep-2.1_3.jarRuntime Bytecode Enhancing 필요 시(선택)

ibatis-sqlmap-2.3.4.726.jar 만이 필수 라이브러리이다. 그러나 일반적으로 commons-dbcp 와 같은 커넥션풀링 라이브러리 및 로깅 처리를 위한 라이브러리는 반드시 필요로 하며, 추가적으로 iBATIS 에서 지원하는 개선된 기능으로 cache 지원 이나 Runtime Bytecode Enhancement 관련 기능을 쓰고자 할 경우는 위의 참조 라이브러리를 추가로 설정할 수 있다. 또한 우리는 Spring-iBATIS 연동 형태의 어플리케이션 개발을 선호하므로 Spring 관련 라이브러리 및 이에 대한 dependency 라이브러리가 일반적으로 함께 포함될 것이다. 여기에 덧붙여 실제 Data Access 처리의 대상의 되는 DBMS(Oracle, Mysql, Hsqldb, Tibero ..) 에 따라 적절한 jdbc 드라이버에 대한 라이브러리가 추가적으로 필요하다.

Step2. sql-map-config.xml 설정 및 기본 Spring 설정

Spring 프레임워크 기반 어플리케이션에서 iBATIS 를 연동하여 사용하고자 하는 경우 Spring의 SqlMapClientFactoryBean 에 대한 설정이 필요하며 여기서는 실제 대상 sql-map-config 설정 파일과 iBATIS 에 제공될 dataSource 에 대한 설정을 지시한다.

<!-- SqlMap setup for iBATIS Database Layer -->
<bean id="sqlMapClient" class="egovframework.rte.psl.orm.ibatis.SqlMapClientFactoryBean">
    <property name="configLocation" value="classpath:/META-INF/sqlmap/sql-map-config.xml"/>
    <property name="dataSource" ref="dataSource"/>
</bean>
  • Spring 연동 기능을 사용하면 iBATIS 의 SqlMapClient(a thread safe client for SQL Maps) 를 별도의 iBATIS API 없이도 얻을 수 있게 된다.
  • 실행환경 3.5 부터는 Spring 4 변경에 따라 org.springframework.orm.ibatis.SqlMapClientFactoryBean 클래스가 egovframework.rte.psl.orm.ibatis.SqlMapClientFactoryBean 로 변경된다.

아래는 주된 iBATIS 의 SQL Map XML Configuration 파일(sql-map-config.xml 설정 파일)이다. iBATIS 단독으로 쓰일 때는 transactionManager, dataSource 설정 등을 추가로 포함해야 하지만 Spring 연동 환경에서는 이 부분은 Spring 이 넘겨주는 dataSource 를 자동으로 사용하게 되고 transaction 관리는 비즈니스 서비스 영역에 선언적으로 설정하여 iBATIS 관련 모듈에서는 고민할 필요가 없게 된다.

<?xml version="1.0" encoding="UTF-8"?>

    <!DOCTYPE sqlMapConfig PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN" 
    "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
 
<sqlMapConfig>
	<settings useStatementNamespaces="false" 
		  ..
	/>
 
	<typeHandler javaType="java.util.Calendar" jdbcType="TIMESTAMP"
		callback="egovframework.rte.psl.dataaccess.typehandler.CalendarTypeHandler" />
 
	<sqlMap resource="META-INF/sqlmap/mappings/testcase-basic.xml" />
	<sqlMap ../>
	..
</sqlMapConfig>
  • sqlMapConfig : iBATIS 설정 파일의 root 태그
  • settings : 다양한 옵션 설정을 지시할 수 있는 태그 (ex. cacheModel 사용여부, Runtime Bytecode Enhance 사용여부, 쿼리문에 대한 Namespace 사용여부 등의 옵션 설정 가능. cf. transaction/dataSource 연결 관련한 설정은 Spring 연동 환경에서는 필요 없음.)
  • typeHandler : javaType ↔ jdbcType 간의 type 변환 처리가 별도로 필요한 경우 typeHandler 를 구현하고 이를 sql-map-config 에 등록함.
  • typeAlias : global 하게 사용할 typeAlias (클래스 풀 패키지명에 대한 간략한 별칭) 를 지정할 수 있음.
  • sqlMap : 각 SQL Mapping XML 파일을 등록함. classpath 나 url 로부터 해당 자원을 stream 형식으로 로딩하게 됨. resource 속성은 기본으로 classpath 경로를 바라본다.

Spring 2.5.5 이상, iBATIS 2.3.2 이상 인 경우에는 iBATIS 연동을 위한 SqlMapClientFactoryBean 정의 시 mappingLocations 속성으로 Sql 매핑 파일에 대한 패턴 표현식으로 일괄 지정도 가능하다. mappingLocations 속성 사용 예는 다음과 같다.

	<!-- SqlMap setup for iBATIS Database Layer -->
	<bean id="sqlMapClient" class="egovframework.rte.psl.orm.ibatis.SqlMapClientFactoryBean">
		<property name="configLocation" value="classpath:/META-INF/sqlmap/sql-map-config.xml"/>
		<!-- Java 1.5 or higher and iBATIS 2.3.2 or higher REQUIRED -->
     		<property name="mappingLocations" value="classpath:/META-INF/sqlmap/mappings/**/*.xml" />
		<property name="dataSource" ref="dataSource"/>
	</bean>

이 경우는 “configLocation” 속성이 필요하지 않지만, 현재 해당 속성이 없는 경우 SqlMapClientFactoryBean의 초기화되지 않기 때문에 “configLocation” 속성을 유지하셔야 한다. 이 때 해당 sql-map-config.xml은 다음과 같이 dummy.xml query를 갖도록 처리하여야 한다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMapConfig PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN" 
"http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
 
<sqlMapConfig>
	<sqlMap resource="sqlmap/sql/common/dummy.xml"/>
</sqlMapConfig>

dummy.xml은 다음과 같이 처리한다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN" "http://www.ibatis.com/dtd/sql-map-2.dtd">
 
<sqlMap namespace="Dummy">
</sqlMap>

Step3. sql mapping xml 설정

iBATIS 에서 정의한 SQL Map 문서 구조 내에서 다양한 옵션 설정과 Mapped statement 정의를 작성하게 된다. 아래는 부서정보에 대한 CRUD 와 관련한 쿼리와 이에 대한 In/Out 객체 맵핑을 포함하는 간략한 매핑 파일이다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN" "http://www.ibatis.com/dtd/sql-map-2.dtd">
 
<sqlMap namespace="Dept">
 
	<typeAlias alias="deptVO" type="egovframework.DeptVO" />
 
	<resultMap id="deptResult" class="deptVO">
		<result property="deptNo" column="DEPT_NO" />
		<result property="deptName" column="DEPT_NAME" />
		<result property="loc" column="LOC" />
	</resultMap>
 
	<insert id="insertDept" parameterClass="deptVO">
		insert into DEPT
		           (DEPT_NO,
		            DEPT_NAME,
		            LOC)
		values     (#deptNo#,
		            #deptName#,
		            #loc#)
	</insert>
 
	<select id="selectDept" parameterClass="deptVO" resultMap="deptResult">
		<![CDATA[
			select DEPT_NO,
			       DEPT_NAME,
			       LOC
			from   DEPT
			where  DEPT_NO = #deptNo#
		]]>
	</select>
 
	<update id="updateDept" parameterClass="deptVO">
		update DEPT
		set    DEPT_NAME = #deptName#,
		       LOC = #loc#
		where  DEPT_NO = #deptNo#
	</update>
 
	<delete id="deleteDept" parameterClass="deptVO">
		delete from DEPT
		where       DEPT_NO = #deptNo#
	</delete>
 
	<select id="selectDeptList" parameterClass="deptVO" resultMap="deptResult">
		<![CDATA[
			select DEPT_NO,
			       DEPT_NAME,
			       LOC
			from   DEPT
			where  1 = 1
		]]>
		<isNotNull prepend="and" property="deptNo">
			DEPT_NO = #deptNo#
		</isNotNull>
		<isNotNull prepend="and" property="deptName">
			DEPT_NAME LIKE '%' || #deptName# || '%'
		</isNotNull>
	</select>
 
</sqlMap>
  • typeAlias : 현재 매핑 파일내에서 객체에 대한 간략한 alias 명을 지정함.
  • resultMap : DB 의 칼럼명과 객체의 Attribute 명에 대한 매핑을 작성함. javaType, jdbcType, columnIndex, typeHandler 등 다양한 추가 옵션 지정이 가능함.
  • insert : 입력 쿼리(insert 문)에 대한 Mapped Statement 정의 태그
  • select : 조회 쿼리에 대한 Mapped Statement 정의 태그
  • update : 수정 쿼리(update 문)에 대한 Mapped Statement 정의 태그
  • delete : 삭제 쿼리(delete 문)에 대한 Mapped Statement 정의 태그

위의 CRUD 관련 매핑 파일에서는 기본으로 DeptVO 라는 JavaBeans 객체를 Parameter/Result 객체로 사용하고 있으며 이를 typeAlias 로 간략하게 지정하여 사용하고 있다. 위에서는 주로 parameterClass 로 지정하여 파라메터 객체에 대한 명시적 사용을 지시하고 있으며, 실제 바인드 변수 처리 시에는 #attribute명# 과 같이 Inline Parameter 형식을 사용하였다. 또한 resultMap 정의를 통하여 ResultSet 에 따른 결과 칼럼정보에 대한 결과 객체(DeptVO)의 필드별 매핑을 별도로 정의하였고 이를 select 문의 resultMap 속성에 명시하여 select 의 결과를 resultMap 을 통해 처리하고 있다. 이러한 방식 외에 다양한 방식으로 Mapped Statement 처리를 정의할 수 있으나, 위와 같이 JavaBeans 객체를 사용하고 또 resultMap 을 정의하여 결과객체 처리를 하며 Inline Parameter 방식으로 바인드 변수 처리하는 스타일로 사용하기를 권고하는 바이다.

Step4. DAO 클래스 생성

간단한 형태의 DAO 클래스를 생성한다. 아래의 DAO 에서 상속하고 있는 EgovAbstractDAO 에서는 SqlMapClientDaoSupport 를 extends 하고 있으며 iBATIS SQL Map 상호 작용을 위한 기본 클래스인 SqlMapClient (위에서 “sqlMapClient” 로 정의한 iBATIS 연동 FactoryBean 에 의해 제공됨) 에 대한 injection 처리가 내부적으로 적용되어 있고, 기본 CRUD 에 대한 iBATIS 실행을 위해서는 간략한 메서드 래핑도 제공한다. 상세 API 를 사용하고자 하는 경우 getSqlMapClientTemplate() 를 통해(ex. getSqlMapClientTemplate().queryWithRowHandler(“selectEmpListToOutFileUsingRowHandler”, paramObject, rowHandler); ) 사용할 수 있다.

DAO 클래스

..
 
@Repository("deptDAO")
public class DeptDAO extends EgovAbstractDAO {
 
    public void insertDept(DeptVO vo) {
        insert("insertDept", vo);
    }
 
    public int updateDept(DeptVO vo) {
        return update("updateDept", vo);
    }
 
    public int deleteDept(DeptVO vo) {
        return delete("deleteDept", vo);
    }
 
    public DeptVO selectDept(DeptVO vo) {
        return (DeptVO)selectByPk("selectDept", vo);
    }
 
    @SuppressWarnings("unchecked")
    public List<DeptVO> selectDeptList(DeptVO searchVO) {
        return list("selectDeptList", searchVO);
    }
}
  • @Repository : DAO 에 대한 @Repository 스테레오 타입 Annotation 을 사용한 Spring Bean 정의

각 CRUD 에 관련한 메서드에서는 queryId 와 파라메터 객체(여기서는 DeptVO) 를 인자로 iBATIS 의 Mapped Statement 을 실행하고 있으며, 조회의 경우에는 단건 조회는 DeptVO 객체로, 리스트 조회는 DeptVO 에 대한 List 를 돌려주도록 하고 있다. iBATIS 내부적으로는 java 1.5 이상의 Generics (type 이 정의된 Collection 처리) 로 처리하지 않으나 sql 매핑 파일에 따라 실제 데이터는 List<DeptVO> 로 처리가 될 것이므로 @SuppressWarnings(“unchecked”) 을 지시하여 호출 이전 모듈에서는 불필요한 Type Casting 을 최소화하고 있다. 만약 sql-map-config.xml 의 settings 옵션으로 useStatementNamespaces=“true” 를 설정한 경우라면 위의 queryId 는 “Dept.insertDept” 와 같이 sql 맵핑 파일에 지정한 Namespace prefix 을 포함하는 형태여야 함에 유의한다.

Step5. 테스트 클래스 생성

위에서 정의한 설정 파일 및 DAO 를 이용하여 간단한 입력,조회 처리에 대한 JUnit TestCase 형태(JUnit 4 스타일)로 구성하였다.

..
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:META-INF/spring/context-*.xml" })
@TransactionConfiguration(transactionManager = "txManager", defaultRollback = false)
@Transactional
public class BasicDataAccessTest {
 
    @Resource(name = "dataSource")
    DataSource dataSource;
 
    @Resource(name = "deptDAO")
    DeptDAO deptDAO;
 
    @Before
    public void onSetUp() throws Exception {
        // 외부에 sql file 로부터 DB 초기화 (기존 테이블 삭제/생성)
        SimpleJdbcTestUtils.executeSqlScript(
            new SimpleJdbcTemplate(dataSource), new ClassPathResource(
                "META-INF/testdata/sample_schema_ddl_hsql.sql"), true);
    }
 
    public DeptVO makeVO() {
        DeptVO vo = new DeptVO();
        vo.setDeptNo(new BigDecimal(90));
        vo.setDeptName("test 부서");
        vo.setLoc("test 위치");
        return vo;
    }
 
    public void checkResult(DeptVO vo, DeptVO resultVO) {
        assertNotNull(resultVO);
        assertEquals(vo.getDeptNo(), resultVO.getDeptNo());
        assertEquals(vo.getDeptName(), resultVO.getDeptName());
        assertEquals(vo.getLoc(), resultVO.getLoc());
    }
 
    @Test
    public void testBasicInsert() throws Exception {
        DeptVO vo = makeVO();
 
        // insert
        deptDAO.insertDept(vo);
 
        // select
        DeptVO resultVO = deptDAO.selectDept(vo);
 
        // check
        checkResult(vo, resultVO);
    }
 
    @Test
    public void testBasicUpdate() throws Exception {
        DeptVO vo = makeVO();
 
        // insert
        deptDAO.insertDept(vo);
 
        // data change
        vo.setDeptName("upd Dept");
        vo.setLoc("upd loc");
 
        // update
        int effectedRows = deptDAO.updateDept(vo);
        assertEquals(1, effectedRows);
 
        // select
        DeptVO resultVO = deptDAO.selectDept(vo);
 
        // check
        checkResult(vo, resultVO);
    }
 
    @Test
    public void testBasicDelete() throws Exception {
        DeptVO vo = makeVO();
 
        // insert
        deptDAO.insertDept(vo);
 
        // delete
        int effectedRows = deptDAO.deleteDept(vo);
        assertEquals(1, effectedRows);
 
        // select
        DeptVO resultVO = deptDAO.selectDept(vo);
 
        // null 이어야 함
        assertNull(resultVO);
    }
 
    @Test
    public void testBasicSelectList() throws Exception {
        DeptVO vo = makeVO();
 
        // insert
        deptDAO.insertDept(vo);
 
        // 검색조건으로 key 설정
        DeptVO searchVO = new DeptVO();
        searchVO.setDeptNo(new BigDecimal(90));
 
        // selectList
        List<DeptVO> resultList = deptDAO.selectDeptList(searchVO);
 
        // key 조건에 대한 결과는 한건일 것임
        assertNotNull(resultList);
        assertTrue(resultList.size() > 0);
        assertEquals(1, resultList.size());
        checkResult(vo, resultList.get(0));
 
        // 검색조건으로 name 설정 - '%' || #deptName# || '%'
        DeptVO searchVO2 = new DeptVO();
        searchVO2.setDeptName(""); // '%' || '' || '%' --> '%%'
 
        // selectList
        List<DeptVO> resultList2 = deptDAO.selectDeptList(searchVO2);
 
        // like 조건에 대한 결과는 한건 이상일 것임
        assertNotNull(resultList2);
        assertTrue(resultList2.size() > 0);
 
    }
}

기본적으로 Annotation 형식 Bean 생성 및 Dependency Injection 을 적용한 Spring 기반의 어플리케이션으로 구성하였으며, dataSource, transactionManager 등의 Spring Bean 이 함께 사용되고 있고, 테스트 케이스는 JUnit 4 형식으로 Spring 설정 파일 로딩 및 transactionManager, dataSource(DB 초기화를 위해 SimpleJdbcTemplate 사용 시 참조)를 얻을 수 있도록 되어 있음을 참고한다. 테스트 편의를 위해 매 테스트 메서드에 우선하여 @Before 로 정의된 메서드에서 기존 테이블 삭제 및 재생성 처리를 하고 있으며, makeVO 라는 별도 메서드로 테스트용 VO 작성 부분을 분리하고, checkResult 라는 메서드로 original VO 와 조회 결과 resultVO 에 대한 assert 비교 로직을 분리하여 재사용하고 있다. @Test 로 지시한 각 메서드가 테스트 메서드이고 입력-조회-결과체크, 입력-변경-조회-결과체크, 입력-삭제-조회-null체크, 검색조건 설정-조회-결과체크 의 flow 에 대한 검증으로 DeptDAO 에 대한 기본 CRUD 테스트 로직을 구성하였다.

Step6. 실 행

  • 파일을 다운로드 받아서 압축을 푼다.
  • 이클립스에서 압축 푼 폴더를 선택하여 프로젝트를 Import 한다.
  • 프로젝트내 src 폴더에 DeptVO.java, DeptDAO.java, BasicDataAccessTest.java, Spring, iBATIS 설정파일 및 Sql 맵핑파일, 테스트 데이터용 초기화 스크립트 파일 및 log4j.xml 가 정상적으로 있는지 확인한다.
  • lib에 라이브러리 파일이 있는지 확인한다.
  • BasicDataAccessTest.java를 선택하여 마우스 오른쪽 클릭하여 Run As > JUnit Test 실행한다.
  • JUnit 결과창에서 정상적으로 수행된 것을 확인한다.

※ 해당 프로젝트는 Hsqldb (현재 메모리구동 방식)을 사용하고 있으나 다른 DBMS 를 쓰고자 하는 경우 jdbc.properties 에 관련 접속정보를 추가하고 JDBC 드라이버 jar 파일을 라이브러리로 추가하여 테스트 해 볼 수 있다.

참고 자료

5.3 - iBATIS Configuration

iBATIS 의 메인 설정 파일인 SQL Map XML Configuration 파일(이하 sql-map-config.xml 설정 파일) 작성과 상세한 옵션 설정에 대해 알아본다.

iBATIS Configuration

iBATIS 의 메인 설정 파일인 SQL Map XML Configuration 파일(이하 sql-map-config.xml 설정 파일) 작성과 상세한 옵션 설정에 대해 알아본다.

sql-map-config.xml

SqlMapClient 설정관련 상세 내역을 제어할 수 있는 메인 설정 파일로 주로 transaction 관리 관련 설정 및 다양한 옵션 설정, Sql Mapping 파일들에 대한 path 설정 등을 포함한다.

Sample Configuration

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMapConfig PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
"http://ibatis.apache.org/dtd/sql-map-config-2.dtd">

 
<sqlMapConfig>
 
	<properties resource="META-INF/spring/jdbc.properties" />
 
	<settings cacheModelsEnabled="true" enhancementEnabled="true"
		lazyLoadingEnabled="true" maxRequests="128" maxSessions="10"
		maxTransactions="5" useStatementNamespaces="false"
		defaultStatementTimeout="1" />
 
	<typeHandler javaType="java.util.Calendar" jdbcType="TIMESTAMP"
		callback="egovframework.rte.psl.dataaccess.typehandler.CalendarTypeHandler" />
 
	<transactionManager type="JDBC">
		<dataSource type="DBCP">
			<property name="driverClassName" value="${driver}" />
			<property name="url" value="${dburl}" />
			<property name="username" value="${username}" />
			<property name="password" value="${password}" />
			<!-- OPTIONAL PROPERTIES BELOW -->
			<property name="maxActive" value="10" />
			<property name="maxIdle" value="5" />
			<property name="maxWait" value="60000" />
			<!-- validation query -->
			<!--<property name="validationQuery" value="select * from DUAL" />-->
			<property name="logAbandoned" value="false" />
			<property name="removeAbandoned" value="false" />
			<property name="removeAbandonedTimeout" value="50000" />
			<property name="Driver.DriverSpecificProperty" value="SomeValue" />
		</dataSource>
	</transactionManager>
 
	<sqlMap resource="META-INF/sqlmap/mappings/testcase-basic.xml" />
	<sqlMap ../>
	..
</sqlMapConfig>
  • properties : 표준 java properties (key=value 형태)파일에 대한 연결을 지원하며 설정 파일내에서 ${key} 와 같은 형태로 properties 형태로 외부화 해놓은 실제의 값(여기서는 DB 접속 관련 driver, url, id/pw)을 참조할 수 있다. resource 속성으로 classpath, url 속성으로 유효한 URL 상에 있는 자원을 지정 가능하다.
  • settings : 이 설정 파일을 통해 생성된 SqlMapClient instance 에 대하여 다양한 옵션 설정을 통해 최적화할 수 있도록 지원한다. 모든 속성은 선택사항(optional) 이다.
속성설명Example, Default
maxRequests같은 시간대에 SQL 문을 실행한 수 있는 thread 의 최대 갯수 지정.maxRequests=“256”, 512
maxSessions주어진 시간에 활성화될 수 있는 session(또는 client) 수 지정.maxSessions=“64”, 128
maxTransactions같은 시간대에 SqlMapClient.startTransaction() 에 들어갈 수 있는 최대 갯수 지정.maxTransactions=“16”, 32
cacheModelsEnabledSqlMapClient 에 대한 모든 cacheModel 에 대한 사용 여부를 global 하게 지정.cacheModelsEnabled=“true”, true (enabled)
lazyLoadingEnabledSqlMapClient 에 대한 모든 lazy loading 에 대한 사용 여부를 global 하게 지정.lazyLoadingEnabled=“true”, true (enabled)
enhancementEnabledruntime bytecode enhancement 기술 사용 여부 지정.enhancementEnabled=“true”, false (disabled)
useStatementNamespacesmapped statements 에 대한 참조 시 namespace 조합 사용 여부 지정. true 인 경우 queryForObject(“sqlMapName.statementName”); 과 같이 사용함.useStatementNamespaces=“false”, false (disabled)
defaultStatementTimeout모든 JDBC 쿼리에 대한 timeout 시간(초) 지정, 각 statement 의 설정으로 override 가능함. 모든 driver가 이 설정을 지원하는 것은 아님에 유의할 것.지정하지 않는 경우 timeout 없음(cf. 각 statement 설정에 따라)
classInfoCacheEnabledintrospected(java 의 reflection API에 의해 내부 참조된) class의 캐쉬를 유지할지에 대한 설정classInfoCacheEnabled=“true”, true (enabled)
statementCachingEnabledprepared statement 의 local cache 를 유지할지에 대한 설정statementCachingEnabled=“true”, true (enabled)
  • typeHandler : javaType ↔ jdbcType 간의 변환(prepared statement 의 파라메터 세팅/resultSet 의 값 얻기)을 처리하는 typeHandler 구현체를 등록할 수 있다.
  • transactionManager : 트랜잭션 관리 서비스를 설정할 수 있다. type 속성으로 어떤 트랜잭션 관리자를 사용할지 지시할 수 있는데, JDBC, JTA, EXTERNAL 의 세가지 트랜잭션 관리자가 프레임워크에 포함되어 있다. 위에서는 일반적인 Connection commit()/rollback() 메서드를 통해 트랜잭션을 관리하는 JDBC 타입으로 설정하였다.
  • dataSource : transactionManager 설정의 일부 영역으로 DataSource 에 대한 설정이다. type 속성으로 어떤 DataSourceFactory 를 사용할지 지시할 수 있는데, SIMPLE, DBCP, JNDI 의 세가지 설정이 가능하다. 위에서는 Apache Commons DBCP(Database Connection Pool) 를 사용하는 DBCP 타입으로 설정하였다. iBATIS 는 DBCP 속성에 대한 설정을 직접 명시할 수 있도록 지원하고 있다. iBATIS 2 버전 이후로는 단일 dataSource 만 지원한다.
  • sqlMap : 명시적으로 각 SQL Map XML 파일을 포함하도록 설정한다. classpath (resource 속성으로 지정) 나 url(url 속성으로 지정) 상의 자원을 stream 형태로 로딩하게 된다. 위에서는 classpath 상에 존재하는 sql 매핑 파일을 지정하였다.

이 외에도 typeAlias(global 한 type 별명-풀패키지명에 비해 간략히), resultObjectFactory (SQL 문의 실행에 의한 결과 객체의 생성을 iBATIS 의 ResultObjectFactory 인터페이스를 구현한 factory 클래스를 통해 처리할 수 있도록 지원) 에 대한 설정이 가능하다. DTD 상 sqlMap 설정은 하나 이상이 필요하고 다른 설정은 선택사항 이다.

SQL Map XML 파일 (sql 매핑 파일)

sql 매핑 파일은 iBATIS 의 mapped statement 형태로 처리될 수 있도록 SQL Map 문서 구조에 따라 다양한 옵션 설정 및 매핑 정의, sql 문을 외부화하여 저장하는 파일이다.

Sample SQL Map XML

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMapConfig PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
 
<sqlMap namespace="Dept">
 
	<typeAlias alias="deptVO" type="egovframework.DeptVO" />
 
	<resultMap id="deptResult" class="deptVO">
		<result property="deptNo" column="DEPT_NO" />
		<result property="deptName" column="DEPT_NAME" />
		<result property="loc" column="LOC" />
	</resultMap>
 
	<insert id="insertDept" parameterClass="deptVO">
		insert into DEPT
		           (DEPT_NO,
		            DEPT_NAME,
		            LOC)
		values     (#deptNo#,
		            #deptName#,
		            #loc#)
	</insert>
 
	<select id="selectDept" parameterClass="deptVO" resultMap="deptResult">
		<![CDATA[
			select DEPT_NO,
			       DEPT_NAME,
			       LOC
			from   DEPT
			where  DEPT_NO = #deptNo#
		]]>
	</select>
 
</sqlMap>
  • typeAlias : 현재 매핑 파일내에서 객체에 대한 간략한 alias 명을 지정함. (cf. 매우 자주 쓰이는 class 의 경우 sql-map-config.xml 에 global 하게 등록하는 것이 좋음)
  • resultMap : DB 칼럼명(select 문의 칼럼 alias) 과 결과 객체의 attribute 에 대한 매핑 및 추가 옵션을 정의함.
  • insert, select : 각 statement 타입에 따른 mapped statement 정의 요소 예시. 유형에 따라 insert/update/delete/select/procedure/statement 요소 사용 가능

이 외에도 parameterMap, resultMap 에 대한 상세 정의, cacheModel 설정, sql 문 재사용을 위한 sql 요소 설정이 나타날 수 있다. 각각에 대한 상세 사항은 관련 가이드를 참고한다.

Mapped Statement

Data Mapper 사상의 핵심으로 Mapped Statement 는 parameter 매핑(input) 과 result 매핑(output) 을 가질 수 있는 어떤 SQL 문이라도 될 수 있다. 단순하게는 파라메터나 결과에 대한 class 를 직접적으로 설정할 수 있으며(권고하지 않는 방법이지만 아예 설정하지 않고 프레임워크에서 제공하는 자동 맵핑 처리도 가능), in/out 매핑, 결과의 cache 유지 등에 대한 상세한 설정이 가능하다.

  • statement 구문
<statement id="statementName"
	   [parameterClass="some.class.Name"]
	   [resultClass="some.class.Name"]
	   [parameterMap="nameOfParameterMap"]
	   [resultMap="nameOfResultMap"]
	   [cacheModel="nameOfCache"]
	   [timeout="5"]>
	select * from PRODUCT where PRD_ID = [?|#propertyName#]
	order by [$simpleDynamic$]
</statement>
  • 위 statement 요소 위치에는 sql 문 유형에 따라 insert, update, delete, select, procedure 가 올 수 있다. 위에서 대괄호[] 내에 있는 속성은 선택 사항이다.
  • parameterClass 나 resultClass 속성으로 In/Out 에 대한 객체를 직접 지정할 수 있다. 이는 일반적으로 표준 JavaBeans 객체 또는 Map (result 인 경우에는 Map 구현체 명시할 것) 이 될수 있으며, 단일 변수인 경우에는 primitive 래퍼 클래스로 지정할 수도 있다.
  • prepared statement 에 대한 바인드 변수 맵핑을 위한 맵핑 정의를 별도의 parameterMap 태그로 따로 지정한 경우 위의 parameterMap 속성으로 해당 id 를 지정한다. parameterMap 을 지정한 경우 sql 문의 바인드 변수 영역은 ? 로 작성하며 parameterMap 매핑 설정의 순서와 갯수가 맞아야 함에 유의한다.
  • parameterMap 을 쓰지 않고 Inline Parameter 로 쓰는 것을 더 선호하며 #속성명# 과 같은 형태로 간략히 기술할 수 있다.
  • resultSet 에 대한 결과 객체 매핑을 위해 별도의 resultMap 태그로 따로 지정한 경우 위의 resultMap 속성으로 해당 id 를 지정한다. resultMap 의 사용은 성능상, 상세 옵션의 적용 기능성 측면에서 추천하는 바이다.
  • 한번 조회 한 결과를 캐슁하기 위해 별도의 cacheModel 태그로 관련 설정을 한 경우, 대상 statement 에 해당 cacheModel id 를 위의 cacheModel 속성과 같이 지시한다. DB 데이터의 변경 시 cache 를 갱신할 수 있도록 cacheModel 설정의 flush 관련 설정에 유의해야 한다.
  • 사용 DBMS 와 JDBC 드라이버가 지원하는 경우 timeout 을 명시할 수 있으며 이는 sql-map-config.xml 의 defaultStatementTimeout 에 우선한다.
  • 위의 order by 절에 쓰인 $변수명$ 은 replaced Text 처리로 input 객체에 해당 변수에 대한 값으로 전달된 String 을 SQL 의 동적 변경 요소로 대치(replace)하여 처리한다. 바인드 변수로 처리할 수 없는 order by 절이나 from 절, 혹은 전체 sql 문의 동적 변경 등을 위해 사용될 수 있으나, SQL Injection 의 보안 위험이 있고, 테이블이나 칼럼이 변경되는 경우 내부적으로 자동 매핑된 결과 객체에 대한 resultSet metadata 의 캐슁내역과 맞지 않아 오류를 일으킬 수 있으므로 유의한다.

5.4 - Spring-iBATIS Integration

Spring은 iBATIS와의 통합을 통해 IoC 및 예외 계층 구조를 활용한 템플릿 스타일 프로그래밍을 지원하며, Spring의 유연한 트랜잭션 처리와 DataSource 설정을 그대로 사용할 수 있다. SqlMapClientFactoryBean은 iBATIS의 SqlMapClient를 생성하고 Spring 컨텍스트에 설정하는 데 사용된다.

Spring-iBATIS Integration

Spring 프레임워크는 iBATIS SQL Map 을 이미 잘 통합하고 있으며, JDBC/Hibernate 에 대한 연동과 동일하게 template 스타일 프로그래밍이 가능토록 지원한다. 이러한 지원으로 Spring 의 특징인 IoC 의 장점과 Exception 계층 구조의 처리가 iBATIS 통합 환경에서도 쉽게 사용되고 있으며, iBATIS 단독 사용 시에 트랜잭션 관리 및 DataSource 에 대한 설정 및 관리가 별도로 필요했던 것에 비해 Spring-iBATIS 통합 환경에서는 Spring 의 유연한 트랜잭션 처리와 dataSource 를 그대로 사용할 수 있다.

Spring 의 SqlMapClientFactoryBean 설정

SqlMapClientFactoryBean 은 iBATIS 의 SqlMapClient 를 생성하는 FactoryBean 구현체로, Spring 의 context 에 iBATIS 의 SqlMapClient 를 셋업하는 일반적인 방법으로 사용된다. 여기서 얻어진 SqlMapClient 는 iBATIS 기반 DAO 에 dependency injection 을 통해 넘겨지게 된다.

Sample Configuration

	<!-- dataSource 설정 -->
	<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
		<property name="driverClassName" value="${driver}"/>
		<property name="url" value="${dburl}"/>
		<property name="username" value="${username}"/>
		<property name="password" value="${password}"/>
		<property name="defaultAutoCommit" value="false"/>
		<property name="poolPreparedStatements" value="true"/>
	</bean>
 
	<!-- SqlMap setup for iBATIS Database Layer -->
	<bean id="sqlMapClient" class="egovframework.rte.psl.orm.ibatis.SqlMapClientFactoryBean">
		<property name="configLocation" value="classpath:/META-INF/sqlmap/sql-map-config.xml"/>
		<property name="dataSource" ref="dataSource"/>
	</bean>
  • dataSource : 데이터베이스 연결 추상화를 제공하는 DataSource 설정. 위에서는 Apache Commons DBCP 를 사용하였으며 DB 접속과 관련된 설정은 property-placeholder 를 사용하여 외부화 하였다.
  • sqlMapClient : Spring의 iBATIS 연동을 위한 SqlMapClientFactoryBean 설정으로 configLocation 속성을 통해 지정한 iBATIS 메인 설정 파일인 sql-map-config.xml 에 대해 iBATIS 의 SqlMapClient instance 를 생성하여 Spring 환경에서 사용 가능토록 한다. Spring 의 dataSource 를 iBATIS 에 넘길 수 있도록 injection 을 지시하고 있으며 이로 인해 iBATIS 설정 파일 자체에서는 dataSource 및 transaction 설정 필요없이(Spring 환경에서는 iBATIS 기반 DAO 호출 이전에 서비스 단에서 선언적인 트랜잭션 처리를 해주었을 것임) Spring 이 제공하는 유연한 dataSource 및 트랜잭션 처리를 사용하게 된다.

Spring 현재 버전에서는 configLocations 속성을 추가하여 sql-map-config.xml 에 대한 패턴 표현식이나 복수 연동(런타임에 하나의 통합 설정으로 merge 됨)도 지원하고 있다. useTransactionAwareDataSource 속성으로 SqlMapClient 에 대해 Spring 이 관리하는 transaction timeout 을 함께 적용할 수 있는 transaction-aware DataSource 를 사용하도록 설정 가능하며(default), lobHandler 속성을 통해 Spring 의 lobHandler 를 설정할 수도 있다.

mappingLocations 지원

또한 iBATIS 사용 환경에서의 중요한 개선 사항으로 mappingLocations 속성을 통해 기존에 iBATIS 메인 설정 파일 내에서 sqlMap 태그로 일일이 지정하여야만 했던 sql 매핑 파일에 대해 Spring 의 SqlMapClientFactoryBean 빈 설정파일에서 Spring 의 유연한 리소스 추상화를 적용하여 리소스 패턴 형태로 일괄 지정이 가능하다. 이 경우 sql 매핑 파일들의 위치는 sql-map-config 설정 파일과 런타임에 merge 되도록 세팅된다. 이 방법은 Spring 2.5.5 이상, iBATIS 2.3.2 이상에서만 지원됨에 유의한다.

	<!-- SqlMap setup for iBATIS Database Layer -->
	<bean id="sqlMapClient" class="egovframework.rte.psl.orm.ibatis.SqlMapClientFactoryBean">
		<property name="configLocation"
			value="classpath:/META-INF/sqlmap/sql-map-config.xml" />
		<property name="mappingLocations"
			value="classpath:/META-INF/sqlmap/mappings/testcase-*.xml" />
		<property name="dataSource" ref="dataSource" />
	</bean>

단 위와 같이 일괄 sql 매핑 파일 지정을 Spring 설정 파일에 지시하였더라도 iBATIS 의 sql-map-config.xml 의 DTD(http://ibatis.apache.org/dtd/sql-map-config-2.dtd) 에 sqlMap 태그가 최소 1개 이상이 나타나야 하도록 지정되어 있으므로 아래와 같이 dummy sql 매핑 파일 하나를 지정하는 sql-map-config.xml 로 작성하면 편할 것이다.

iBATIS 의 설정 변경

  • sqlMap 설정을 제거한(dummy 하나는 설정 필요) sql-map-config.xml
 
<!DOCTYPE sqlMapConfig PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"
"http://ibatis.apache.org/dtd/sql-map-config-2.dtd">
<sqlMapConfig>
	<settings useStatementNamespaces="false" 
				defaultStatementTimeout="10"
	/>
 
	<typeHandler javaType="java.util.Calendar" jdbcType="TIMESTAMP"
		callback="egovframework.rte.psl.dataaccess.typehandler.CalendarTypeHandler" />
 
	<!-- Spring 2.5.5 이상, iBATIS 2.3.2 이상에서는 iBATIS 연동을 위한 SqlMapClientFactoryBean 정의 시 
               mappingLocations 속성으로 Sql 매핑 파일의 일괄 지정이 가능하다. 
	    ("sqlMapClient" bean 설정 시  
                       mappingLocations="classpath:/META-INF/sqlmap/mappings/testcase-*.xml" 로 지정하였음)
	    단, sql-map-config-2.dtd 에서 sqlMap 요소를 하나 이상 지정하도록 되어 있으므로 
              아래의 dummy 매핑 파일을 설정하였다.
	-->
	<sqlMap resource="META-INF/sqlmap/mappings/testcase-dummy.xml" />
 
</sqlMapConfig>
  • testcase-dummy.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE sqlMap PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN" "http://www.ibatis.com/dtd/sql-map-2.dtd">
 
<sqlMap namespace="Dummy"/>

iBATIS-based DAO

Spring 의 SqlMapClientDaoSupport 클래스는 iBATIS 의 SqlMapClient data access object 를 위한 편리한 수퍼 클래스로 이를 extends 하는 서브 클래스에 SqlMapClientTemplate 를 제공한다. SqlMapClientTemplate 는 iBATIS 를 통한 data access 를 단순화하는 헬퍼 클래스로 SQLException 을 Spring dao 의 exception Hierarchy 에 맞게 unchecked DataAccessException 으로 변환해 주며 Spring 의 JdbcTemplate 과 동일한 처리 구조의 SQLExceptionTranslator 를 사용할 수 있게 해준다. 또한 iBATIS 의 SqlMapExecutor 의 실행 메서드에 대한 편리한 mirror 메서드를 다양하게 제공하므로 일반적인 쿼리나 insert/update/delete 처리에 대해 편리하게 사용할 수 있도록 권고된다. 그러나 batch update 와 같은 복잡한 수행에 대해서는 Spring 의 SqlMapClientCallback 에 대한 명시적인 구현(보통 anonymous inner class 로 작성)이 필요하다.

public class SqlMapAccountDao extends SqlMapClientDaoSupport implements AccountDao {
 
    public Account getAccount(String email) throws DataAccessException {
        return (Account) getSqlMapClientTemplate().queryForObject("getAccountByEmail", email);
    }
 
    public void insertAccount(Account account) throws DataAccessException {
        getSqlMapClientTemplate().update("insertAccount", account);
    }
}

iBATIS 연동 DAO 는 SqlMapClientDaoSupport 를 extends 하고 있으며, getSqlMapClientTemplate() 를 통해 SqlMapClientTemplate 를 얻어 iBATIS 의 data access 처리를 래핑하여 실행토록 처리하고 있다.

  <bean id="accountDao" class="example.SqlMapAccountDao">
    <property name="sqlMapClient" ref="sqlMapClient"/>
  </bean>

iBATIS 연동 DAO 에 sqlMapClient 빈을 주입해 주어야 한다.

위의 처리는 Annotation 을 사용한 빈 생성 및 dependency 처리 시 sqlMapClient 의 DAO 주입 설정에 어려움이 존재한다. 이의 손쉬운 해결을 위해 전자정부 프레임워크는 EgovAbstractDAO 를 확장하여 제공하고 있다.

5.5 - iBATIS에서의 Data Type 매핑

Java 애플리케이션에서 데이터베이스와 상호작용할 때, Java 타입과 DBMS의 JDBC 타입 간의 정확한 매핑이 중요하다. iBATIS는 JavaBeans 객체의 속성과 DB 테이블의 컬럼 타입을 매핑하여 데이터 바인딩 및 매핑을 처리하며, 이를 통해 다양한 데이터 타입에 대한 적절한 사용 방법을 제공한다.

Data Type

어플리케이션을 작성할 때 Data Type 에 대한 올바른 사용과 관련 처리는 매우 중요하다. 특히 데이터베이스를 이용하여 데이터를 저장하고 조회할 때 Java 어플리케이션에서의 Type 과 DBMS 에서 지원하는 관련 매핑 jdbc Type 의 정확한 사용이 필요하며, 여기에서는 iBATIS 환경에서 javaType 과 특정 DBMS 의 jdbcType 의 적절한 매핑 사용예를 중심으로 일반적인 Data Type 의 사용 가이드를 참고할 수 있도록 한다.

기본 Data Type 사용 방법

iBATIS SQL Mapper 프레임워크는 Java 어플리케이션 영역의 표준 JavaBeans 객체(또는 Map 등)의 각 Attribute 에 대한 Java Type 과 JDBC 드라이버에서 지원하는 각 DBMS의 테이블 칼럼에 대한 Data Type 의 매핑을 기반으로 parameter / result 객체에 대한 바인딩/매핑 을 처리한다. 각 javaType 에 대한 매칭되는 jdbcType 은 일반적인 Ansi SQL 을 사용한다고 하였을 때 아래에서 대략 확인할 수 있을 것이다. 특정 DBMS 벤더에 따라 추가적으로 지원/미지원 하는 jdbcType 이 다를 수 있고, 또한 같은 jdbcType 을 사용한다 하더라도 타입에 따른 사용 가능한 경계값(boundary max/min value)은 다를 수 있다.

아래에서는 다양한 primitive 타입과 숫자 타입, 문자 타입, 날짜 타입에 대한 기본 insert/select 를 통해 iBATIS 사용 환경에서의 data type 에 대한 사용 예를 알아보겠다.

Sample Type VO

public class TypeTestVO implements Serializable {
 
    private static final long serialVersionUID = -3653247402772333834L;
 
    private int id;
 
    private BigDecimal bigdecimalType;
 
    private boolean booleanType;
 
    private byte byteType;
 
    private String charType;
 
    private double doubleType;
 
    private float floatType;
 
    private int intType;
 
    private long longType;
 
    private short shortType;
 
    private String stringType;
 
    private Date dateType;
 
    private java.sql.Date sqlDateType;
 
    private Time sqlTimeType;
 
    private Timestamp sqlTimestampType;
 
    private Calendar calendarType;
 
    public int getId() {
        return id;
    }
 
    public void setId(int id) {
        this.id = id;
    }
 
    public BigDecimal getBigdecimalType() {
        return bigdecimalType;
    }
 
    public void setBigdecimalType(BigDecimal bigdecimalType) {
        this.bigdecimalType = bigdecimalType;
    }
 
    public boolean isBooleanType() {
        return booleanType;
    }
 
    public void setBooleanType(boolean booleanType) {
        this.booleanType = booleanType;
    }
 
    public byte getByteType() {
        return byteType;
    }
 
    public void setByteType(byte byteType) {
        this.byteType = byteType;
    }
 
    public String getCharType() {
        return charType;
    }
 
    public void setCharType(String charType) {
        this.charType = charType;
    }
 
    public double getDoubleType() {
        return doubleType;
    }
 
    public void setDoubleType(double doubleType) {
        this.doubleType = doubleType;
    }
 
    public float getFloatType() {
        return floatType;
    }
 
    public void setFloatType(float floatType) {
        this.floatType = floatType;
    }
 
    public int getIntType() {
        return intType;
    }
 
    public void setIntType(int intType) {
        this.intType = intType;
    }
 
    public long getLongType() {
        return longType;
    }
 
    public void setLongType(long longType) {
        this.longType = longType;
    }
 
    public short getShortType() {
        return shortType;
    }
 
    public void setShortType(short shortType) {
        this.shortType = shortType;
    }
 
    public String getStringType() {
        return stringType;
    }
 
    public void setStringType(String stringType) {
        this.stringType = stringType;
    }
 
    public Date getDateType() {
        return dateType;
    }
 
    public void setDateType(Date dateType) {
        this.dateType = dateType;
    }
 
    public java.sql.Date getSqlDateType() {
        return sqlDateType;
    }
 
    public void setSqlDateType(java.sql.Date sqlDateType) {
        this.sqlDateType = sqlDateType;
    }
 
    public Time getSqlTimeType() {
        return sqlTimeType;
    }
 
    public void setSqlTimeType(Time sqlTimeType) {
        this.sqlTimeType = sqlTimeType;
    }
 
    public Timestamp getSqlTimestampType() {
        return sqlTimestampType;
    }
 
    public void setSqlTimestampType(Timestamp sqlTimestampType) {
        this.sqlTimestampType = sqlTimestampType;
    }
 
    public Calendar getCalendarType() {
        return calendarType;
    }
 
    public void setCalendarType(Calendar calendarType) {
        this.calendarType = calendarType;
    }
 
}

위 TypeTestVO 의 각 attribute 는 다양한 data Type 에 대한 사용예의 샘플이며 이에 대한 매핑 jdbc 타입은 아래의 각 DBMS 별 DDL 을 통해 일차적으로 살펴보자.

Sample TYPETEST Table Hsqldb DDL script

create table TYPETEST (
    id numeric(10,0) not null,
    bigdecimal_type numeric(19,2),
    boolean_type boolean, 
    byte_type tinyint,
    char_type char(1),
    double_type double,
    float_type float,
    int_type integer,
    long_type bigint,
    short_type smallint,
    string_type varchar(255),
    date_type date,
    sql_date_type datetime,
    sql_time_type time,
    sql_timestamp_type timestamp,
    calendar_type timestamp,
    primary key (id)
);

위 create sql 문의 hsqldb 의 예이며, 실제로 Ansi SQL 의 Data Type 에 대한 표준을 잘 따르는 예이다. boolean 타입을 직접 지원하고 있으며 tinyint, double, date, time 등 다양한 jdbcType 에 대하여 사용에 특별한 무리가 없음을 아래의 테스트 케이스로 알 수 있을 것이다.

Sample SQL Mapping XML

<sqlMap namespace="TypeTest">
 
	<typeAlias alias="typeTestVO"
		type="egovframework.rte.psl.dataaccess.vo.TypeTestVO" />
 
	<!-- CalendarTypeHandler 는 sql-map-config.xml 에 등록하였음 -->
	<typeAlias alias="calendarTypeHandler" type="egovframework.rte.psl.dataaccess.typehandler.CalendarTypeHandler"/>
 
	<resultMap id="typeTestResult" class="typeTestVO">
		<result property="id" column="ID" />
		<result property="bigdecimalType" column="BIGDECIMAL_TYPE" />
		<result property="booleanType" column="BOOLEAN_TYPE" />
		<result property="byteType" column="BYTE_TYPE" />
		<result property="charType" column="CHAR_TYPE" />
		<result property="doubleType" column="DOUBLE_TYPE" />
		<result property="floatType" column="FLOAT_TYPE" />
		<result property="intType" column="INT_TYPE" />
		<result property="longType" column="LONG_TYPE" />
		<result property="shortType" column="SHORT_TYPE" />
		<result property="stringType" column="STRING_TYPE" />
		<result property="dateType" column="DATE_TYPE" />
		<result property="sqlDateType" column="SQL_DATE_TYPE" />
		<result property="sqlTimeType" column="SQL_TIME_TYPE" />
		<result property="sqlTimestampType" column="SQL_TIMESTAMP_TYPE" />
		<result property="calendarType" column="CALENDAR_TYPE" typeHandler="calendarTypeHandler" />
	</resultMap>
 
	<insert id="insertTypeTest" parameterClass="typeTestVO">
		<![CDATA[
			insert into TYPETEST
			           (ID,
			            BIGDECIMAL_TYPE,
			            BOOLEAN_TYPE,
			            BYTE_TYPE,
			            CHAR_TYPE,
			            DOUBLE_TYPE,
			            FLOAT_TYPE,
			            INT_TYPE,
			            LONG_TYPE,
			            SHORT_TYPE,
			            STRING_TYPE,
			            DATE_TYPE,
			            SQL_DATE_TYPE,
			            SQL_TIME_TYPE,
			            SQL_TIMESTAMP_TYPE,
			            CALENDAR_TYPE)
			values     (#id#,
			            #bigdecimalType#,
			            #booleanType#,
			            #byteType#,
			            #charType:CHAR#,
			            #doubleType#,
			            #floatType#,
			            #intType#,
			            #longType#,
			            #shortType#,
			            #stringType#,
			            #dateType#,
			            #sqlDateType#,
			            #sqlTimeType#,
			            #sqlTimestampType#,
			            #calendarType,handler=calendarTypeHandler#)
		]]>
	</insert>
 
	<select id="selectTypeTest" parameterClass="typeTestVO"
		resultMap="typeTestResult">
		<![CDATA[
			select ID,
			       BIGDECIMAL_TYPE,
			       BOOLEAN_TYPE,
			       BYTE_TYPE,
			       CHAR_TYPE,
			       DOUBLE_TYPE,
			       FLOAT_TYPE,
			       INT_TYPE,
			       LONG_TYPE,
			       SHORT_TYPE,
			       STRING_TYPE,
			       DATE_TYPE,
			       SQL_DATE_TYPE,
			       SQL_TIME_TYPE,
			       SQL_TIMESTAMP_TYPE,
			       CALENDAR_TYPE
			from   TYPETEST
			where  ID = #id#
		]]>
	</select>
 
</sqlMap>

TypeTestVO JavaBeans 객체를 통해 insert/select 를 처리하는 sql 매핑 xml 이다. resultMap 을 정의하여 select 결과 객체 매핑을 처리하고 있으며, 입력 및 조회 조건 의 파라메터 바인딩을 Inline Parameter 방법을 통해 처리하고 있다. resultMap 이나 parameterMap(Inline Parameter 도 마찬가지) 에서는 javaType=“string”, jdbcType=“VARCHAR” 와 같이 명시적으로 java/jdbc type 에 대한 지시를 할 수도 있다. (성능상으로는 추천, 그러나 실제와 맞지 않는 type 지시는 런타임에 오류 발생) 또한, 위의 Inline parameter 처리 시 calendar 속성에 대해 handler=calendarTypeHandler 로 지시한 것과 resultMap 처리 시 typeHandler=“calendarTypeHandler” 로 지시한 것에서 확인할 수 있듯이 일반적인 java-jdbc 매핑으로 커버하지 못하는 부분에 대하여 사용자가 typeHandler 를 구현하여 타입 컨버전에 대한 로직 처리를 제공함으로써 위와 같이 calendar type ↔ TIMESTAMP 변환이 가능한 것처럼 확장할 수도 있다.

위에서 TypeTestVO 와 SQL Mapping XML 은 아래의 추가적인 DBMS 테스트 시 변경없이 재사용하였고, 일부 Data Type 의 미지원/DBMS 별 매핑타입 사용/경계값 상이함 등에 대해서는 DDL / TestCase 에서 약간의 로직 분기나 회피를 통해 문제되는 부분을 피하고 전체적인 관점에서 재사용 할 수 있도록 테스트 하였으므로 참고하기 바란다.

Sample TestCase

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:META-INF/spring/context-*.xml" })
@TransactionConfiguration(transactionManager = "txManager", defaultRollback = false)
@Transactional
public class DataTypeTest extends TestBase {
 
    @Resource(name = "typeTestDAO")
    TypeTestDAO typeTestDAO;
 
    @Before
    public void onSetUp() throws Exception {
 
        // 외부에 sql file 로부터 DB 초기화 (TypeTest 기존 테이블 삭제/생성)
        SimpleJdbcTestUtils.executeSqlScript(
            new SimpleJdbcTemplate(dataSource), new ClassPathResource(
                "META-INF/testdata/sample_schema_ddl_typetest_" + usingDBMS
                    + ".sql"), true);
    }
 
    public TypeTestVO makeVO() throws Exception {
        TypeTestVO vo = new TypeTestVO();
        vo.setId(1);
        vo.setBigdecimalType(new BigDecimal("99999999999999999.99"));
        vo.setBooleanType(true);
        vo.setByteType((byte) 127);
        // VO 에서 String 으로 선언했음. char 로 하고자 하는 경우 TypeHandler 작성 필요
        vo.setCharType("A");
        // Oracle 10g 에서 double precision 타입은 Double.MAX_VALUE 를 수용하지 못함.
        // oracle jdbc driver 에서 Double.MAX_VALUE 를 전달하면 Overflow Exception trying to bind 1.7976931348623157E308 에러 발생
        // mysql 5.0 에서 테스트 시 Double.MAX_VALUE 입력은 가능하나 조회 시 Infinity 로 되돌려짐
        // tibero - Double.MAX_VALUE 입력 시 Exception 발생
        vo.setDoubleType(isHsql ? Double.MAX_VALUE : 1.7976931348623157d);
        // mysql 5.0 에서 테스트 시 Float.MAX_VALUE 를 입력할 수 없음
        vo.setFloatType(isMysql ? (float) 3.40282 : Float.MAX_VALUE);
        vo.setIntType(Integer.MAX_VALUE);
        vo.setLongType(Long.MAX_VALUE);
        vo.setShortType(Short.MAX_VALUE);
        vo.setStringType("abcd가나다라あいうえおカキクケコ");
        SimpleDateFormat sdf =
            new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
        vo.setDateType(sdf.parse("2009-02-18"));
        long currentTime = new java.util.Date().getTime();
        vo.setSqlDateType(new java.sql.Date(currentTime));
        vo.setSqlTimeType(new java.sql.Time(currentTime));
        vo.setSqlTimestampType(new java.sql.Timestamp(currentTime));
        vo.setCalendarType(Calendar.getInstance());
 
        return vo;
    }
 
    public void checkResult(TypeTestVO vo, TypeTestVO resultVO) {
        assertNotNull(resultVO);
        assertEquals(vo.getId(), resultVO.getId());
        assertEquals(vo.getBigdecimalType(), resultVO.getBigdecimalType());
        assertEquals(vo.getByteType(), resultVO.getByteType());
        // mysql 인 경우 timestamp 칼럼에 null 을 입력하면 현재 시각으로 insert 됨에 유의
        if (vo.getCalendarType() == null && isMysql) {
            assertNotNull(resultVO.getCalendarType());
            // mysql 인 경우 java 의 timestamp 에 비해 3자리 정밀도 떨어짐
        } else if (vo.getCalendarType() != null && isMysql) {
            String orgSeconds =
                Long.toString(vo.getCalendarType().getTime().getTime());
            String mysqlSeconds =
                Long.toString(resultVO.getCalendarType().getTime().getTime());
            assertEquals(orgSeconds.substring(0, orgSeconds.length() - 3),
                mysqlSeconds.substring(0, mysqlSeconds.length() - 3));
        } else {
            assertEquals(vo.getCalendarType(), resultVO.getCalendarType());
        }
        assertEquals(vo.getCharType(), resultVO.getCharType());
        assertEquals(vo.getDateType(), resultVO.getDateType());
        // double 에 대한 delta 를 1e-15 로 주었음.
        assertEquals(vo.getDoubleType(), resultVO.getDoubleType(), isMysql
            ? 1e-14 : 1e-15);
        // float 에 대한 delta 를 1e-7 로 주었음.
        assertEquals(vo.getFloatType(), resultVO.getFloatType(), 1e-7);
        assertEquals(vo.getIntType(), resultVO.getIntType());
        assertEquals(vo.getLongType(), resultVO.getLongType());
        assertEquals(vo.getShortType(), resultVO.getShortType());
        // java.sql.Date 의 경우 Date 만 비교
        if (vo.getSqlDateType() != null) {
            assertEquals(vo.getSqlDateType().toString(), resultVO
                .getSqlDateType().toString());
        }
 
        // mysql 인 경우 timestamp 칼럼에 null 을 입력하면 현재 시각으로 insert 됨에 유의
        if (vo.getSqlTimestampType() == null && isMysql) {
            assertNotNull(resultVO.getSqlTimestampType());
        } else if (vo.getCalendarType() != null && isMysql) {
            String orgSeconds =
                Long.toString(vo.getSqlTimestampType().getTime());
            String mysqlSeconds =
                Long.toString(resultVO.getSqlTimestampType().getTime());
            assertEquals(orgSeconds.substring(0, orgSeconds.length() - 3),
                mysqlSeconds.substring(0, mysqlSeconds.length() - 3));
        } else {
            assertEquals(vo.getSqlTimestampType(), resultVO
                .getSqlTimestampType());
        }
        // java.sql.Time 의 경우 Time 만 비교
        if ((isHsql || isOracle || isTibero || isMysql)
            && vo.getSqlTimeType() != null) {
            assertEquals(vo.getSqlTimeType().toString(), resultVO
                .getSqlTimeType().toString());
        } else {
            assertEquals(vo.getSqlTimeType(), resultVO.getSqlTimeType());
        }
        assertEquals(vo.getStringType(), resultVO.getStringType());
        assertEquals(vo.isBooleanType(), resultVO.isBooleanType());
 
    }
 
    @Test
    public void testDataTypeTest() throws Exception {
        // 값을 세팅하지 않고 insert 해 봄 - id 는 int 의 초기값에 따라 0 임
        TypeTestVO vo = new TypeTestVO();
 
        // insert
        typeTestDAO.insertTypeTest("insertTypeTest", vo);
 
        // select
        TypeTestVO resultVO = typeTestDAO.selectTypeTest("selectTypeTest", vo);
 
        // check
        checkResult(vo, resultVO);
 
        try {
            // duplication 테스트
            typeTestDAO.insertTypeTest("insertTypeTest", vo);
 
            fail("키 값 duplicate 에러가 발생해야 합니다.");
        } catch (Exception e) {
            assertNotNull(e);
            assertTrue(e instanceof DataIntegrityViolationException);
            assertTrue(e.getCause() instanceof SQLException);
        }
 
        // DataType 테스트 데이터 입력 및 재조회
        vo = makeVO();
 
        // insert
        typeTestDAO.insertTypeTest("insertTypeTest", vo);
 
        // select
        resultVO = typeTestDAO.selectTypeTest("selectTypeTest", vo);
 
        // check
        checkResult(vo, resultVO);
 
    }
 
}

위에서는 TypeTestVO 의 각 속성에 값을 세팅하지 않고 입력/조회, 키 값 중복 시 spring 의 DataIntegrityViolationException 이 발생하는지, 각 속성에 테스트 데이터(경계값 또는 의미있는 사용예 로써의 값)를 세팅하여 입력/조회 에 대한 처리를 확인해 봄으로써 java ↔ DBMS 의 타입 매핑의 예를 확인해 보았다. 특히 위의 makeVO 메서드 에서는 특정 javaType 에 대한 DBMS 의 db type 에 따라 경계값의 max value 가 달라질 수 있음을 확인할 수 있으며, checkResult 메서드에서는 특히 날짜 처리 타입과 관련하여 DBMS 에 따라 null 입력일 때 초기값 이나, 지원하는 정밀도(입력시 java 의 Date 류에서는 년월일시분초 를 넘어 상세하게 표현한 입력값 javaType 에 대해 jdbcType 의 결과 조회 시 날짜, 또는 시각 정보만으로 제한된다던지, 초 레벨의 정밀도가 java 에 비해 낮다던지)의 차이가 있음을 확인할 수 있다. java-jdbc type 에 대한 일반적인 매핑은 위 Hsqldb 의 예를 기본으로 이해하면 적합할 것으로 보며, 아래에서 특정 DBMS 의 DDL 예를 통해 각 데이터베이스 환경에서 Data Type 사용의 참고가 될 수 있기 바란다.

Sample TYPETEST Table Oracle (10gR2 기준 테스트) DDL script

create table TYPETEST (
    id number(10,0) not null,
    bigdecimal_type number(19,2),
    boolean_type number(1,0), 
    byte_type number(3,0),
    char_type char(1),
    double_type double precision,
    float_type float,
    int_type number(10,0),
    long_type number(19,0),
    short_type number(5,0),
    string_type varchar2(255),
    date_type date,
    sql_date_type date,
    sql_time_type timestamp,
    sql_timestamp_type timestamp,
    calendar_type timestamp,
    primary key (id)
);

Sample TYPETEST Table Mysql (5.X) DDL script

create table TYPETEST (
    id numeric(10,0) not null,
    bigdecimal_type numeric(19,2),
    boolean_type boolean, 
    byte_type tinyint,
    char_type char(1),
    double_type double,
    float_type float,
    int_type integer,
    long_type bigint,
    short_type smallint,
    string_type varchar(255),
    date_type date,
    sql_date_type datetime,
    sql_time_type time,
    sql_timestamp_type timestamp,
    calendar_type timestamp,
    primary key (id)
);

Sample TYPETEST Table Tibero(3.x) DDL script

create table TYPETEST (
    id numeric(10,0) not null,
    bigdecimal_type number(19,2),
    boolean_type number(1), 
    byte_type number(3),
    char_type char(1),
    double_type double precision,
    float_type float,
    int_type integer,
    long_type integer, /* integer 가 bigint 의 범위까지 포함함 */
    short_type smallint,
    string_type varchar(255),
    date_type date,
    sql_date_type date,
    sql_time_type date,	/* time, timestamp 으로 설정 시 문제발생 */
    sql_timestamp_type timestamp,
    calendar_type timestamp,
    primary key (id)
);

5.6 - iBATIS parameterMap

parameterMap은 Java 객체의 속성에 대한 매핑을 설정하여 SQL 바인드 변수를 처리하는 요소로, 기술적인 매핑이 필요한 경우에 사용되지만, Dynamic 요소와 함께 사용할 수 없고 바인드 변수의 순서를 맞춰야 하는 제한이 있어 일반적으로 추천되지 않는다. 대신 parameterClass나 Inline Parameter 방식이 더 자주 사용된다.

parameterMap

parameterMap 은 해당 요소로 SQL 문 외부에 정의한 입력 객체의 속성에 대한 name 및 javaType, jdbcType 을 비롯한 옵션을 설정할 수 있는 매핑 요소이다. 이를 통해 JavaBeans 객체(또는 Map 등)에 대한 prepared statement 에 대한 바인드 변수 매핑을 처리할 수 있다. 유사한 기능을 처리하는 parameterClass 나 Inline Parameter 에 비해 많이 사용되지 않지만 더 기술적인(descriptive) parameterMap(예를 들어 stored procedure 를 위한) 이 필요하거나, XML 의 일관된 사용과 순수성을 지키고자 할때 좋은 접근법이 될 수도 있다. 그러나 Dynamic 요소와 함께 사용될 수 없고 바인드 변수의 갯수와 순서를 정확히 맞춰야 하는 불편이 있는 등 일반적으로 사용을 추천하지 않는다.

기본 parameterMap 사용 방법

아래의 샘플 parameterMap 정의를 참고하라.

Sample parameterMap

	..
	<typeAlias alias="empVO" type="egovframework.rte.psl.dataaccess.vo.EmpVO" />
 
	<parameterMap id="empParam" class="empVO">
		<parameter property="empNo" javaType="decimal" jdbcType="NUMERIC" />
		<parameter property="empName" javaType="string" jdbcType="VARCHAR" nullValue="blank" />
		<parameter property="job" javaType="string" jdbcType="VARCHAR" nullValue="" />
		<parameter property="mgr" javaType="decimal" jdbcType="NUMERIC" />
		<parameter property="hireDate" javaType="date" jdbcType="DATE" />
		<parameter property="sal" javaType="decimal" jdbcType="NUMERIC" />
		<parameter property="comm" javaType="decimal" jdbcType="NUMERIC" nullValue="-99999" />
		<parameter property="deptNo" javaType="decimal" jdbcType="NUMERIC" />
	</parameterMap>
 
	<insert id="insertEmpUsingParameterMap" parameterMap="empParam">
		<![CDATA[
			insert into EMP
			           (EMP_NO,
			            EMP_NAME,
			            JOB,
			            MGR,
			            HIRE_DATE,
			            SAL,
			            COMM,
			            DEPT_NO)
			values     (?,
			            ?,
			            ?,
			            ?,
			            ?,
			            ?,
			            ?,
			            ?)
		]]>
	</insert>

위 sql 매핑 파일에서 parameterMap 요소로 empParam 이라는 id 를 부여하고 대상 입력 객체는 EmpVO 를 지정하고 있다. EmpVO 에 대한 상세 attribute (속성) 들에 대해 parameter 하위 요소로 매핑 정의를 하고 있는데 이때 추가적으로 javaType, jdbcType 에 대해 위에서는 명시하였다. (java 의 reflection 기술을 사용하여 대상 클래스의 개별 속성에 대한 type 을 구하는 것보다 직접 type 에 대한 지시를 설정으로 명시하므로 약간의 성능상 이점이 있을 수 있다.) 위에서는 동일한 property 가 중복으로 사용되는 경우가 없으나 parameterMap 은 아래의 insertEmpUsingParameterMap mapped statement 예에서 보듯이 ? 에 대한 순서대로 매핑되므로 만약 중복으로 사용되는 경우가 필요하다면 parameterMap 정의부터 순서를 맞추어 동일한 property 의 중복 정의가 필요할 수 있다. 또한 nullValue 속성을 지정한 property 에 대해서는 해당 값이 nullValue 에 지정된 값으로 전달되는 경우 데이터베이스에는 null 로 처리가 이 외에도 typeName, resultMap, mode, typeHandler, numericScale 에 대한 속성 정의가 가능하다.

Sample TestCase

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath*:META-INF/spring/context-*.xml" })
@TransactionConfiguration(transactionManager = "txManager", defaultRollback = false)
@Transactional
public class ParameterMapTest extends TestBase {
 
    @Resource(name = "empDAO")
    EmpDAO empDAO;
 
    @Before
    public void onSetUp() throws Exception {
        // DB 초기화
    }
 
    public EmpVO makeVO() throws ParseException {
        EmpVO vo = new EmpVO();
        vo.setEmpNo(new BigDecimal(9000));
        vo.setEmpName("test Emp");
        vo.setJob("test Job");
 
        // 7839,'KING','PRESIDENT'
        vo.setMgr(new BigDecimal(7839));
        SimpleDateFormat sdf =
            new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
        vo.setHireDate(sdf.parse("2009-02-09"));
        // mysql 5.0.X에서는 소숫점 자릿수 만큼 .00 이 달려 나와 테스트 편의상 소숫점 자리수가 없도록 칼럼 선언 하였음.
        if (isMysql) {
            vo.setSal(new BigDecimal("12345"));
            vo.setComm(new BigDecimal(100));
        } else {
            vo.setSal(new BigDecimal("12345.67"));
            vo.setComm(new BigDecimal(100.00));
        }
        // 10,'ACCOUNTING','NEW YORK'
        vo.setDeptNo(new BigDecimal(10));
        return vo;
    }
 
    public void checkResult(EmpVO vo, EmpVO resultVO) {
        assertNotNull(resultVO);
        assertEquals(vo.getEmpNo(), resultVO.getEmpNo());
        assertEquals(vo.getEmpName(), resultVO.getEmpName());
        assertEquals(vo.getJob(), resultVO.getJob());
        assertEquals(vo.getMgr(), resultVO.getMgr());
        assertEquals(vo.getHireDate(), resultVO.getHireDate());
        assertEquals(vo.getSal(), resultVO.getSal());
        assertEquals(vo.getComm(), resultVO.getComm());
        assertEquals(vo.getDeptNo(), resultVO.getDeptNo());
    }
 
    @Test
    public void testParameterMapInsert() throws Exception {
        EmpVO vo = makeVO();
 
        // insert
        empDAO.insertEmp("insertEmpUsingParameterMap", vo);
 
        // select
        EmpVO resultVO = empDAO.selectEmp("selectEmp", vo);
 
        // check
        checkResult(vo, resultVO);
    }
 
    @Test
    public void testParameterMapInsertWithNullValue() throws Exception {
        EmpVO vo = new EmpVO();
        // key 설정
        vo.setEmpNo(new BigDecimal(9000));
 
        // parameterMap nullValue test
        vo.setEmpName("blank");
        vo.setJob("");
        // cf.) -99999.99 는 NumberFormatException 임을
        // 확인하였음!
        vo.setComm(new BigDecimal("-99999"));
 
        // insert
        empDAO.insertEmp("insertEmpUsingParameterMap", vo);
 
        // select
        EmpVO resultVO = empDAO.selectEmp("selectEmp", vo);
 
        // check
        assertNotNull(resultVO);
        assertEquals(vo.getEmpNo(), resultVO.getEmpNo());
        // parameterMap 설정에서 nullValue="blank" .. 에 따라
        // 해당값이 null 로 입력되었을 것임
        assertNull(resultVO.getEmpName());
        assertNull(resultVO.getJob());
        assertNull(resultVO.getComm());
    }
 
}

위에서 parameterMap 을 이용한 파라메터 객체 바인딩을 처리하는 insertEmpUsingParameterMap 쿼리문에 대해 입력객체를 세팅 후 입력/조회 처리하고 있는 테스트 케이스이다. 실제로 매핑 파일 내에서 parameterMap 을 사용하는 경우와 Inline parameter 또는 parameterClass 를 그대로 쓰는 경우에서도 어플리케이션 영역은 동일한 형태의 입력 객체를 인자로 iBATIS API 를 호출하게 된다. 위의 testParameterMapInsertWithNullValue 테스트 메서드에서는 parameterMap 에 nullValue 속성으로 지정한 특정한 값을 세팅 하므로써 DB 에 null 로 입력이 된 결과를 조회하여 assertNull 로 확인하고 있다.

5.7 - Inline Parameters

Inline Parameters는 #property# 노테이션을 사용해 간편하게 바인드 변수 매핑을 처리하며, 별도의 parameterMap 선언 없이 입력 객체의 속성을 SQL에 직접 매핑할 수 있다. Dynamic 요소와 함께 사용 가능하며, 필요한 경우 JDBC 타입과 null 값을 추가 노테이션으로 지정할 수 있다.

Inline Parameters

이전에 살펴본 prepared statement 에 대한 바인드 변수 매핑 처리를 위한 parameterMap 요소(SQL 문 외부에 정의한 입력 객체 property name 및 javaType, jdbcType 을 비롯한 옵션을 설정 매핑 요소) 와 동일한 기능을 처리하는 간편한 방법을 Inline Parameters 방법으로 제공한다. 보통 parameterClass 로 명시된 입력 객체에 대해 바인드 변수 영역을 간단한 #property# 노테이션으로 나타내는 Inline Parameter 방법은 기존 parameterMap 에서의 ? 와 이의 순서를 맞춘 외부 parameterMap 선언으로 처리하는 방법에 비해 많이 사용되고 일반적으로 추천하는 방법이다. 이는 Dynamic 요소와 함께 사용될 수 있고 별도의 외부 매핑 정의 없이 바인드 변수 처리가 필요한 위치에 해당 property 를 직접 사용 가능하며, 필요한 경우 jdbcType 이나 nullValue 를 간단한 추가 노테이션(ex. #empName:VARCHAR:blank# ) 와 같이 지정할 수 있고, 상세한 옵션이 필요한 경우에는 (ex. #comm,javaType=decimal,jdbcType=NUMERIC,nullValue=-99999# ) 와 같이 ,(comma) 로 구분된 필요한 속성=값 을 상세하게 기술할 수도 있다.

기본 Inline Parameters 사용 방법

아래의 샘플 Inline Parameters 사용 쿼리문을 참고하라.

Sample Inline Parameters

	..
	<typeAlias alias="empVO" type="egovframework.rte.psl.dataaccess.vo.EmpVO" />
 
	<insert id="insertEmptUsingInLineParam">
		<![CDATA[
			insert into EMP
			           (EMP_NO,
			            EMP_NAME,
			            JOB,
			            MGR,
			            HIRE_DATE,
			            SAL,
			            COMM,
			            DEPT_NO)
			values     (#empNo:NUMERIC#,
			            #empName:VARCHAR:blank#,
			            #job:VARCHAR:""#,	/* inline parameter 에서는 empty String 을 nullValue로 대체할 수 없음 - cf.) oracle인 경우는 "" 가 null 임 */
			            #mgr:NUMERIC#,
			            #hireDate:DATE#,
			            #sal:NUMERIC#,	
			            #comm,javaType=decimal,jdbcType=NUMERIC,nullValue=-99999#,
			            #deptNo:NUMERIC#)
		]]>
	</insert>

위 sql 매핑 파일에서 별도의 parameterMap 외부 정의없이 #property:jdbcType# 형태로 직접 바인드 변수 영역에 나타내고 있다. #property# 만 나타내도 iBATIS 에서 자동으로 타입처리는 잘된다. 위에서는 해당 쿼리에 parameterClass=“empVO” 에 대한 입력 객체 설정이 나타나 있지 않음에도 iBATIS 에서 런타임에 인자로 주어지는 입력 객체를 자동으로 파악하여 파라메터 처리를 하고 있음을 확인할 수 있다. 그러나 parameterClass 의 명시는 성능 상 반드시 추천하는 바이다.

  • #property#
  • #property:jdbcType#
  • #property:jdbcType:nullValue# 형태까지 : 를 구분자로 함께 나타낼 수 있으며 nullValue 대체 값은 jdbcType 명시가 반드시 필요하다.

이 외에도 속성=값 형태의 옵션 설정을 , 를 구분자로 아래와 같이 나타낼 수 있다.

  • #propertyName,javaType=?,jdbcType=?,mode=?,nullValue=?,handler=?,numericScale=?#

위에서 ? 부분을 해당 속성에 따라 적절한 값으로 설정하면 된다. mode 는 stored procedure 의 IN/OUT/INOUT 모드를 지시할 수 있는 속성이고 numericScale 은 stored procedure 의 OUT/INOUT 변수가 decimal 이나 numeric인 경우 DBMS 의 Scale 정보를 유지하기 위해 명시해야 하는 속성이다. handler 속성에는 typeHandler 를 지시할 수 있다.

Sample TestCase

..
   public EmpVO makeVO() throws ParseException {
        EmpVO vo = new EmpVO();
        vo.setEmpNo(new BigDecimal(9000));
        vo.setEmpName("test Emp");
        vo.setJob("test Job");
        // 7839,'KING','PRESIDENT'
        vo.setMgr(new BigDecimal(7839));
        SimpleDateFormat sdf =
            new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
        vo.setHireDate(sdf.parse("2009-02-09"));
 
        // mysql 은 소숫점 이하 자리가 .00 으로 기본 들어가게 되어 테스트 편의상 numeric(5) 로 선언하였음.
        if (isMysql) {
            vo.setSal(new BigDecimal("12345"));
            vo.setComm(new BigDecimal(100));
        } else {
            vo.setSal(new BigDecimal("12345.67"));
            vo.setComm(new BigDecimal(100.00));
        }
        // 10,'ACCOUNTING','NEW YORK'
        vo.setDeptNo(new BigDecimal(10));
        return vo;
    }
 
    public void checkResult(EmpVO vo, EmpVO resultVO) {
        assertNotNull(resultVO);
        assertEquals(vo.getEmpNo(), resultVO.getEmpNo());
        assertEquals(vo.getEmpName(), resultVO.getEmpName());
        assertEquals(vo.getJob(), resultVO.getJob());
        assertEquals(vo.getMgr(), resultVO.getMgr());
        assertEquals(vo.getHireDate(), resultVO.getHireDate());
        assertEquals(vo.getSal(), resultVO.getSal());
        assertEquals(vo.getComm(), resultVO.getComm());
        assertEquals(vo.getDeptNo(), resultVO.getDeptNo());
    }
 
    @Test
    public void testInLineParameterInsert() throws Exception {
        EmpVO vo = makeVO();
 
        // insert
        empDAO.insertEmp("insertEmptUsingInLineParam", vo);
 
        // select
        EmpVO resultVO = empDAO.selectEmp("selectEmp", vo);
 
        // check
        checkResult(vo, resultVO);
    }
 
    @Test
    public void testInLineParameterInsertWithNullValue() throws Exception {
        EmpVO vo = new EmpVO();
        // key 설정
        vo.setEmpNo(new BigDecimal(9000));
 
        // inline parameter nullValue test
        vo.setEmpName("blank");
        // inline parameter 에서는 empty String 을
        // nullValue로 대체할 수 없음
        // ref.)
        // http://www.nabble.com/inline-map-format%3A-empty-String-in-nullValue-td18905940.html
        vo.setJob(""); // cf.) oracle 인 경우 "" 는 null 과
        // 같음!
        vo.setComm(new BigDecimal("-99999"));
 
        // insert
        empDAO.insertEmp("insertEmptUsingInLineParam", vo);
 
        // select
        EmpVO resultVO = empDAO.selectEmp("selectEmp", vo);
 
        // check
        assertNotNull(resultVO);
        assertEquals(vo.getEmpNo(), resultVO.getEmpNo());
        // inline parameter 설정에서
        // #empName:VARCHAR:blank# ..
        // 에 따라 해당값이 null 로
        // 입력되었을 것임
        assertNull(resultVO.getEmpName());
        // inline parameter 에서는 empty String 을
        // nullValue로 대체할 수 없음 확인!
        // cf.) parameterMap 케이스에서는
        // assertNull(resultVO.getJob()) // cf.) oracle
        // 인 경우 "" 는 null 과 같음!
        // assertNotNull(resultVO.getJob());
        assertNull(resultVO.getComm());
    }
..

위에서 Inline Parameters 을 이용한 파라메터 객체 바인딩을 처리하는 insertEmptUsingInLineParam 쿼리문에 대해 입력객체를 세팅 후 입력/조회 처리하고 있는 테스트 케이스이다. 실제로 위 어플리케이션 영역은 parameterMap 사용 시와 차이가 없음을 알 수 있다. 위의 testInLineParameterInsertWithNullValue 테스트 메서드에서는 Inline Parameters 에 nullValue 속성으로 지정한 특정한 값을 세팅 함으로써 DB 에 null 로 입력이 된 결과를 조회하여 assertNull 로 확인하고 있다. 위에서 empty String 의 nullValue 설정은 inline parameter 노테이션의 한계로 제대로 처리되지 않음을 확인하였으므로 참고하기 바란다.

5.8 - resultMap

resultMap은 SQL 결과를 Java 객체의 속성에 어떻게 매핑할지 상세하게 제어할 수 있는 매핑 요소로, 칼럼 타입 지정, null 값 대체, 타입 핸들러 처리, 복합 객체 매핑 등을 지원한다. 이는 자동 매핑 방식보다 더 복잡한 매핑을 처리할 수 있어 많이 사용된다.

resultMap

resultMap 은 SQL 문 외부에 정의한 매핑 요소로, result set 으로부터 어떻게 데이터를 뽑아낼지, 어떤 칼럼을 어떤 property로 매핑할지에 대한 상세한 제어를 가능케 해준다. resultMap 은 일반적으로 가장 많이 사용되는 중요한 매핑 요소로 resultClass 속성을 이용한 자동 매핑 접근법에 비교하여 칼럼 타입의 지시, null value 대체값, typeHandler 처리, complex property 매핑(다른 JavaBean, Collections 등을 포함하는 복합 객체) 등을 허용한다.

기본 resultMap 사용 방법

아래의 샘플 resultMap 정의를 참고하라.

Sample resultMap

	..
	<typeAlias alias="empVO" type="egovframework.rte.psl.dataaccess.vo.EmpVO" />
 
	<resultMap id="empResult" class="empVO" >
		<result property="empNo" column="EMP_NO" columnIndex="1"
			javaType="decimal" jdbcType="NUMERIC" />
		<result property="empName" column="EMP_NAME" columnIndex="2"
			javaType="string" jdbcType="VARCHAR" />
		<result property="job" column="JOB" columnIndex="3" javaType="string"
			jdbcType="VARCHAR" />
		<result property="mgr" column="MGR" columnIndex="4" javaType="decimal"
			jdbcType="NUMERIC" />
		<result property="hireDate" column="HIRE_DATE" columnIndex="5"
			javaType="date" jdbcType="DATE" />
		<result property="sal" column="SAL" columnIndex="6" javaType="decimal"
			jdbcType="NUMERIC" />
		<result property="comm" column="COMM" columnIndex="7" javaType="decimal"
			jdbcType="NUMERIC" nullValue="0" />
		<result property="deptNo" column="DEPT_NO" columnIndex="8"
			javaType="decimal" jdbcType="NUMERIC" />
	</resultMap>
 
	<select id="selectEmpUsingResultMap" parameterClass="empVO" resultMap="empResult">
		<![CDATA[
			select EMP_NO,
			       EMP_NAME,
			       JOB,
			       MGR,
			       HIRE_DATE,
			       SAL,
			       COMM,
			       DEPT_NO
			from   EMP
			where  EMP_NO = #empNo#
		]]>
	</select>

위 sql 매핑 파일에서 resultMap 요소로 empResult 라는 id 를 부여하고 대상 결과 객체는 EmpVO 를 지정하고 있다. EmpVO 에 대한 상세 attribute (속성) 들에 대해 result 하위 요소로 매핑 정의를 하고 있는데 column 속성으로 result set 에서 얻을 수 있는 select 대상 칼럼(column alias 를 쓴 경우이면 해당 alias 명) 을 매핑하게 된다. 위에서는 추가적으로 columnIndex, javaType, jdbcType 에 대해 명시하였다. 위와 같이 타입을 명확하게 지시해주면 java 의 reflection 기술을 사용하여 대상 클래스의 개별 속성에 대한 type 을 구하는 것보다 성능상 이점이 있을 수 있다. columnIndex 를 지정하는 경우에는 rs.getString(“EMP_NAME”) → rs.getString(2)로 처리되는 사소한 성능상의 이점이 있지만, 순서의 지정이나 설정 자체의 번거로움으로 추천하지 않는 바이다. 또한 nullValue 속성을 지정한 property 에 대해서는 해당 값이 데이터베이스에서 null 로 읽혔을 때 nullValue 에 지정된 값으로 대체되어 JavaBeans property 에 설정된다. 이 외에도 result 하위 요소의 속성으로 select, resultMap 을 통해 다른 쿼리문의 결과나 complex property 의 처리를 위한 내포 객체에 대한 외부 resultMap 매핑요소를 참조할 수 있다. 또한 typeHandler 속성을 통해 iBATIS 의 기본 처리가 아닌 custom typeHandler 구현체를 지시할 수도 있다.

  • resultMap structure
	<resultMap id="resultMapName" class="some.domain.Class" 
		[extends="parent-resultMap"] [groupBy="some property list"]>
		<result property="propertyName" column="COLUMN_NAME" 
			[columnIndex="1"] [javaType="int"] [jdbcType="NUMERIC"] [nullValue="-999999"]
			[select="someOtherStatement"] [resultMap="someOtherResultMap"]
			[typeHandler="com.mydomain.MyTypehandler"] />
		<result  />
		<result  />
		<result  />
	</resultMap>

위에서 resultMap 태그의 extends 속성을 명시하면 외부에 정의한 다른 resultMap 을 상속(관련 property - column 매핑을 현재 resultMap 에 정의하지 않고도)할 수 있으며, groupBy 속성을 사용하여 nested resultMap 에서의 N+1 쿼리 문제를 풀수 있도록 해당 속성을 통해 명시한 property 리스트의 값이 같은 row 들에 대해 하나의 결과 객체로 생성해 주게 된다.

Sample TestCase

..
    @Test
    public void testResultMapSelect() throws Exception {
        EmpVO vo = new EmpVO();
        // 7369,'SMITH','CLERK',7902,'1980-12-17',800,NULL,20
        vo.setEmpNo(new BigDecimal(7369));
 
        // select
        EmpVO resultVO = empDAO.selectEmp("selectEmpUsingResultMap", vo);
 
        // check
        assertNotNull(resultVO);
        assertEquals(new BigDecimal(7369), resultVO.getEmpNo());
        assertEquals("SMITH", resultVO.getEmpName());
        assertEquals("CLERK", resultVO.getJob());
        assertEquals(new BigDecimal(7902), resultVO.getMgr());
        SimpleDateFormat sdf =
            new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
        assertEquals(sdf.parse("1980-12-17"), resultVO.getHireDate());
        assertEquals(new BigDecimal(800), resultVO.getSal());
 
        // nullValue test - <result property="comm" column="COMM" .. nullValue="0" />
        assertEquals(new BigDecimal(0), resultVO.getComm());
        assertEquals(new BigDecimal(20), resultVO.getDeptNo());
    }

위에서 resultMap 을 이용한 결과 객체 매핑을 처리하는 selectEmpUsingResultMap 쿼리문에 대해 조회조건(pk) 를 세팅한 입력 객체를 인자로 조회 처리하고 있는 테스트 케이스이다. 실제로 매핑 파일 내에서 resultMap 을 사용하는 경우와 resultClass 를 그대로 쓰는 경우에서도 어플리케이션 영역은 동일한 형태의 결과 객체를 얻을 수 있지만 sql 매핑 파일내에서 resultMap 의 정의와 사용은 많은 장점이 있으므로 추천된다. 위의 테스트 검증을 통해 EmpVO 의 결과 객체로 조회 결과가 잘 반영되었음을 확인할 수 있고 nullValue 속성으로 지정한 comm attribute 에 대해서는 데이터베이스는 null 이지만 0 에 해당하는 numeric 값으로 대체되어 조회되었음을 확인할 수 있다.

resultMap 상속

아래의 샘플 resultMap 정의를 참고하라.

Sample VO

..
public class EmpExtendsDeptVO extends DeptVO {
 
    private static final long serialVersionUID = -4653117983538108612L;
 
    private BigDecimal empNo;
 
    private String empName;
 
    private String job;
 
    private BigDecimal mgr;
 
    private Date hireDate;
 
    private BigDecimal sal;
 
    private BigDecimal comm;
 
    public BigDecimal getEmpNo() {
        return empNo;
    }
 
    public void setEmpNo(BigDecimal empNo) {
        this.empNo = empNo;
    }
..

Sample resultMap (extends)

	..
	<typeAlias alias="empExtendsDeptVO" type="egovframework.rte.psl.dataaccess.vo.EmpExtendsDeptVO" />
 
	<!--
		cf.) VO 의 상속관계와 resultMap의 상속관계가 같을 필요는 없음. 아래의 empExtendsDeptResult 가
		empResult (Emp 속성을 가지고 있는 위의 resultMap)를 extends 하고 있지만 실제
		EmpExtendsDeptVO 는 DeptVO 를 extends 하면서 child가 Emp 속성을 가지게끔 define 했음을 볼
		수 있음
	--> 
	<resultMap id="empExtendsDeptResult" class="empExtendsDeptVO" extends="empResult">
		<!--<result property="deptNo" column="DEPT_NO"/>-->
		<result property="deptName" column="DEPT_NAME"/>
		<result property="loc" column="LOC"/>
	</resultMap>
 
	<select id="selectEmpExtendsDeptUsingResultMap" parameterClass="empVO" resultMap="empExtendsDeptResult">
		<![CDATA[
			select EMP_NO,
			       EMP_NAME,
			       JOB,
			       MGR,
			       HIRE_DATE,
			       SAL,
			       COMM,
			       A.DEPT_NO,
			       B.DEPT_NAME,
			       B.LOC
			from   EMP A, DEPT B
			where  A.DEPT_NO = B.DEPT_NO
			and    EMP_NO = #empNo#
		]]>
	</select>

empExtendsDeptResult resultMap 은 위의 기본적인 resultMap 사용 방법에서 정의한 empResult resultMap 을 extends 하고 있으며, 추가적인 deptName, loc 에 대한 property 매핑만을 추가하여 실제로 emp 관련 property 들에 대해서는 extends 하고 있는 매핑 정의를 따라 자동으로 처리가 됨을 확인할 수 있다. resultMap 에 대한 매핑 정의의 상속은 결과 객체 JavaBeans 의 상속 관계와는 상관없이 별개로 이루어진다. (위에서는 VO extends 코드와 resultMap extends 가 반대임)

Sample TestCase

..
    @Test
    public void testExtendsResultMapSelect() throws Exception {
        EmpVO vo = new EmpVO();
        // 7369,'SMITH','CLERK',7902,'1980-12-17',800,NULL,20
        vo.setEmpNo(new BigDecimal(7369));
 
        // select
        EmpExtendsDeptVO resultVO =
            empDAO.selectEmpExtendsDept("selectEmpExtendsDeptUsingResultMap",
                vo);
 
        // check
        assertNotNull(resultVO);
        // resultMap extends test (extends empResult)
        assertEquals(new BigDecimal(7369), resultVO.getEmpNo());
        assertEquals("SMITH", resultVO.getEmpName());
        assertEquals("CLERK", resultVO.getJob());
        assertEquals(new BigDecimal(7902), resultVO.getMgr());
        SimpleDateFormat sdf =
            new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
        assertEquals(sdf.parse("1980-12-17"), resultVO.getHireDate());
        assertEquals(new BigDecimal(800), resultVO.getSal());
        // nullValue test - <result property="comm" column="COMM" .. nullValue="0" />
        assertEquals(new BigDecimal(0), resultVO.getComm());
        assertEquals(new BigDecimal(20), resultVO.getDeptNo());
 
        assertEquals("RESEARCH", resultVO.getDeptName());
        assertEquals("DALLAS", resultVO.getLoc());
 
    }

위에서 extends 속성을 통해 상위 resultMap 을 상속하는 empExtendsDeptResult resultMap 을 이용한 결과 객체 매핑을 처리하는 selectEmpExtendsDeptUsingResultMap 에 대해 조회 처리하고 있는 테스트 케이스이다. 위의 테스트 검증을 통해 EmpExtendsDeptVO 의 결과 객체로 조회 결과가 잘 반영되었음을 확인할 수 있고 parent resultMap 에 존재하는 nullValue 대체 처리도 잘 반영됨을 확인할 수 있다.

Simple Composite resultMap

아래의 샘플 resultMap 정의를 참고하라.

Sample VO

..
public class EmpDeptSimpleCompositeVO implements Serializable {
 
    private static final long serialVersionUID = -8049578957221741495L;
 
    private BigDecimal empNo;
 
    private String empName;
 
    private String job;
 
    private BigDecimal mgr;
 
    private Date hireDate;
 
    private BigDecimal sal;
 
    private BigDecimal comm;
 
    private BigDecimal deptNo;
 
    private String deptName;
 
    private String loc;
 
    public BigDecimal getEmpNo() {
        return empNo;
    }
 
    public void setEmpNo(BigDecimal empNo) {
        this.empNo = empNo;
    }
..

별도의 parent - child extends 구조를 사용하지 않고 단순히 attributes 를 통합한 VO 이다.

Sample resultMap

	..
	<typeAlias alias="empDeptSimpleCompositeVO" type="egovframework.rte.psl.dataaccess.vo.EmpDeptSimpleCompositeVO" />
 
	<resultMap id="empDeptSimpleComposite" class="empDeptSimpleCompositeVO" >
		<result property="empNo" column="EMP_NO"/>
		<result property="empName" column="EMP_NAME"/>
		<result property="job" column="JOB"/>
		<result property="mgr" column="MGR"/>
		<result property="hireDate" column="HIRE_DATE"/>
		<result property="sal" column="SAL"/>
		<result property="comm" column="COMM" nullValue="0"/>
		<result property="deptNo" column="DEPT_NO"/>
		<result property="deptName" column="DEPT_NAME"/>
		<result property="loc" column="LOC"/>
	</resultMap>
 
	<select id="selectEmpDeptSimpleCompositeUsingResultMap" parameterClass="empVO" resultMap="empDeptSimpleComposite">
		<![CDATA[
			select EMP_NO,
			       EMP_NAME,
			       JOB,
			       MGR,
			       HIRE_DATE,
			       SAL,
			       COMM,
			       A.DEPT_NO,
			       B.DEPT_NAME,
			       B.LOC
			from   EMP A, DEPT B
			where  A.DEPT_NO = B.DEPT_NO
			and    EMP_NO = #empNo#
		]]>
	</select>

EMP 와 DEPT 에 대한 조인 쿼리를 통하여 DEPT 정보를 포함하는 결과 row 를 조회하는 쿼리문은 위에서와 완전히 동일하며, extends 를 사용하는 empExtendsDeptResult resultMap 과 비교하여 resultMap 정의가 모든 요소를 포함하고 있다. 위에서는 조회 필드가 결과 객체의 property 가 동일하므로 extends 를 사용하는 resultMap 으로 단순히 resultClass 만 맞춰주어 변경 가능할 것이다.

Sample TestCase

..
    @Test
    public void testSimpleCompositeResultMapSelect() throws Exception {
        EmpVO vo = new EmpVO();
        // 7369,'SMITH','CLERK',7902,'1980-12-17',800,NULL,20
        vo.setEmpNo(new BigDecimal(7369));
 
        // select
        EmpDeptSimpleCompositeVO resultVO =
            empDAO.selectEmpDeptSimpleComposite(
                "selectEmpDeptSimpleCompositeUsingResultMap", vo);
 
        // check
        assertNotNull(resultVO);
        assertEquals(new BigDecimal(7369), resultVO.getEmpNo());
        assertEquals("SMITH", resultVO.getEmpName());
        assertEquals("CLERK", resultVO.getJob());
        assertEquals(new BigDecimal(7902), resultVO.getMgr());
        SimpleDateFormat sdf =
            new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
        assertEquals(sdf.parse("1980-12-17"), resultVO.getHireDate());
        assertEquals(new BigDecimal(800), resultVO.getSal());
        // nullValue test - <result property="comm" column="COMM" .. nullValue="0" />
        assertEquals(new BigDecimal(0), resultVO.getComm());
        assertEquals(new BigDecimal(20), resultVO.getDeptNo());
        assertEquals("RESEARCH", resultVO.getDeptName());
        assertEquals("DALLAS", resultVO.getLoc());
 
    }

extends 없이 모든 매핑 요소를 simple 하게 모두 정의한 resultMap 을 이용한 결과 객체 매핑을 처리하는 selectEmpDeptSimpleCompositeUsingResultMap 에 대해 조회 처리하고 있는 테스트 케이스이다. 위의 테스트 검증을 통해 EmpDeptSimpleCompositeV 의 결과 객체로 조회 결과가 잘 반영되었음을 확인할 수 있다.

위의 비교 구현을 통해 join 조회를 통해 얻어지는 결과 객체가 단순 composite VO 형태인 경우 resultMap extends 을 사용하면 좀더 쉽게 매핑 처리할 수 있을 것으로 보여진다.

Complex Properties resultMap 사용 방법

아래의 1:1, 1:N, 1:N(N+1 select), Hierarchical relation 에 대한 샘플 resultMap 정의를 참고하라.

Complext Properties - (1:1 관계) resultMap


..
public class EmpIncludesDeptVO implements Serializable {
 
    private static final long serialVersionUID = -4113989804152701350L;
 
    private BigDecimal empNo;
 
    private String empName;
 
    private String job;
 
    private BigDecimal mgr;
 
    private Date hireDate;
 
    private BigDecimal sal;
 
    private BigDecimal comm;
 
    private BigDecimal deptNo;
 
    // EMP - DEPT 1:1 relation
    private DeptVO deptVO;
 
    public BigDecimal getEmpNo() {
        return empNo;
    }
 
    public void setEmpNo(BigDecimal empNo) {
        this.empNo = empNo;
    }
..
    public DeptVO getDeptVO() {
        return deptVO;
    }
 
    public void setDeptVO(DeptVO deptVO) {
        this.deptVO = deptVO;
    }
}

위의 EmpIncludesDeptVO 는 DeptVO 를 1:1 관계의 멤버 attribute 로 포함하고 있다.

<sqlMap namespace="EmpComplexResult">
	..
	<typeAlias alias="empIncludesDeptVO" type="egovframework.rte.psl.dataaccess.vo.EmpIncludesDeptVO" />
 
	<resultMap id="empIncludesDeptResult" class="empIncludesDeptVO">
		<result property="empNo" column="EMP_NO" />
		<result property="empName" column="EMP_NAME" />
		<result property="job" column="JOB" />
		<result property="mgr" column="MGR" />
		<result property="hireDate" column="HIRE_DATE" />
		<result property="sal" column="SAL" />
		<result property="comm" column="COMM" nullValue="0" />
		<result property="deptNo" column="DEPT_NO" />
 
		<!--
			Emp-Dept 1:1 relation
			테스트 결과 resultMap 의 참조 시 sql-map-config.xml 의
			useStatementNamespaces="false" 와 상관없이 namespace prefix 를 써야 하는듯
		-->
		<result property="deptVO" resultMap="EmpComplexResult.getDeptResult" />
	</resultMap>
 
	<resultMap id="getDeptResult" class="deptVO">
		<result property="deptNo" column="DEPT_NO" />
		<result property="deptName" column="DEPT_NAME" />
		<result property="loc" column="LOC" />
	</resultMap>
 
	<select id="selectEmpIncludesDeptResultUsingResultMap" parameterClass="empVO" resultMap="empIncludesDeptResult">
		<![CDATA[
			select EMP_NO,
			       EMP_NAME,
			       JOB,
			       MGR,
			       HIRE_DATE,
			       SAL,
			       COMM,
			       A.DEPT_NO as DEPT_NO,
			       B.DEPT_NAME,
			       B.LOC
			from   EMP A, DEPT B
			where  A.DEPT_NO = B.DEPT_NO
			and    EMP_NO = #empNo#
		]]>
	</select>

위 sql 매핑 파일에서 1:1 관계를 표현하는 Complex Properties 를 포함하는 EmpIncludesDeptVO 에 대한 resultMap 매핑 처리 시 해당 객체의 deptVO 멤버 attribute 에 대한 매핑 정의를 위해 resultMap=“EmpComplexResult.getDeptResult” 과 같이 DeptVO 에 대한 외부 resultMap 을 재사용하며 참조하고 있다. iBATIS 는 nested resultMap 에 대한 매핑 정의를 참고하여 join 쿼리에 의한 결과 칼럼을 자동으로 복합 객체(특히 DeptVO 관련 멤버 객체에)에 매핑해 주게 된다.

Sample TestCase

..
    @Test
    public void testComplexPropertiesOneToOneResultMapSelect() throws Exception {
        EmpVO vo = new EmpVO();
        // 7369,'SMITH','CLERK',7902,'1980-12-17',800,NULL,20
        vo.setEmpNo(new BigDecimal(7369));
 
        // select
        EmpIncludesDeptVO resultVO =
            empDAO.selectEmpDeptComplexProperties(
                "selectEmpIncludesDeptResultUsingResultMap", vo);
 
        // check
        assertNotNull(resultVO);
        assertEquals(new BigDecimal(7369), resultVO.getEmpNo());
        assertEquals("SMITH", resultVO.getEmpName());
        assertEquals("CLERK", resultVO.getJob());
        assertEquals(new BigDecimal(7902), resultVO.getMgr());
        SimpleDateFormat sdf =
            new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
        assertEquals(sdf.parse("1980-12-17"), resultVO.getHireDate());
        assertEquals(new BigDecimal(800), resultVO.getSal());
        assertEquals(new BigDecimal(0), resultVO.getComm());
        assertEquals(new BigDecimal(20), resultVO.getDeptNo());
        // 1:1 relation included DeptVO
        assertEquals(new BigDecimal(20), resultVO.getDeptVO().getDeptNo());
        assertEquals("RESEARCH", resultVO.getDeptVO().getDeptName());
        assertEquals("DALLAS", resultVO.getDeptVO().getLoc());
    }

위의 테스트 케이스 검증 코드에서 resultVO.getDeptVO().getXXX 와 같이 내포 객체의 각 attribute 가 잘 설정되었음을 확인할 수 있다.

Complext Properties - (1:N 관계) resultMap

..
public class DeptIncludesEmpListVO implements Serializable {
 
    private static final long serialVersionUID = -3369530755443065377L;
 
    private BigDecimal deptNo;
 
    private String deptName;
 
    private String loc;
 
    private List<EmpVO> empVOList;
 
    public BigDecimal getDeptNo() {
        return deptNo;
    }
 
    public void setDeptNo(BigDecimal deptNo) {
        this.deptNo = deptNo;
    }
..
    public List<EmpVO> getEmpVOList() {
        return empVOList;
    }
 
    public void setEmpVOList(List<EmpVO> empVOList) {
        this.empVOList = empVOList;
    }
}

위의 DeptIncludesEmpListVO 는 EmpVO 의 List 를 1:N 관계의 멤버 attribute 로 포함하고 있다.

<sqlMap namespace="EmpComplexResult">
	..
	<typeAlias alias="deptIncludesEmpListVO" type="egovframework.rte.psl.dataaccess.vo.DeptIncludesEmpListVO" />
 
	<!-- 1:N 인 경우 groupBy 속성을 명시 -->
	<resultMap id="deptIncludesEmpListResult" class="deptIncludesEmpListVO" groupBy="deptNo">
		<result property="deptNo" column="DEPT_NO" />
		<result property="deptName" column="DEPT_NAME" />
		<result property="loc" column="LOC" />
 
		<!-- Dept-EmpList 1:N relation -->
		<result property="empVOList" resultMap="EmpComplexResult.getEmpResult" />
	</resultMap>
 
	<resultMap id="getEmpResult" class="empVO">
		<result property="empNo" column="EMP_NO" />
		<result property="empName" column="EMP_NAME" />
		<result property="job" column="JOB" />
		<result property="mgr" column="MGR" />
		<result property="hireDate" column="HIRE_DATE" />
		<result property="sal" column="SAL" />
		<result property="comm" column="COMM" nullValue="0" />
		<result property="deptNo" column="DEPT_NO" />
	</resultMap>
 
	<select id="selectDeptIncludesEmpListResultUsingResultMap" parameterClass="deptVO" resultMap="deptIncludesEmpListResult">
		<![CDATA[
			select   A.DEPT_NO as DEPT_NO,
			         DEPT_NAME,
			         LOC,
			         EMP_NO,
			         EMP_NAME,
			         JOB,
			         MGR,
			         HIRE_DATE,
			         SAL,
			         COMM
			from     DEPT A,
			         EMP B
			where    A.DEPT_NO = B.DEPT_NO
			         and A.DEPT_NO = #deptNo#
			order by B.EMP_NO
		]]>
	</select>

위 sql 매핑 파일에서 1:N 관계를 표현하는 Complex Properties 를 포함하는 DeptIncludesEmpListVO 에 대한 resultMap 매핑 처리 시 해당 객체의 empVOList 멤버 attribute 에 대한 매핑 정의를 위해 resultMap=“EmpComplexResult.getEmpResult” 과 같이 EmpVO 에 대한 외부 resultMap 을 재사용하며 참조하고 있다. 이때 위의 쿼리에서는 join 에 따라 조회되는 row 수가 1:1 관계와 같이 단건이 아니라 같은 DEPT_NO 에 대해 복수 개의 결과가 얻어지는데 이에 대한 resultMap 정의 시 groupBy=“deptNo” 을 지정하였으므로 같은 deptNo 인 다건의 EmpVO 에 대한 List 가 복합 객체의 List 멤버 attribute 에 설정되어 얻어진다. iBATIS 는 nested resultMap 에 대한 매핑 정의를 참고하여 join 쿼리에 의한 결과 칼럼을 자동으로 복합 객체에 매핑해 주게 되며 여기에서와 같이 groupBy 로 지정한 property 로 그룹핑하여 하위 요소를 List 형태로 자동 세팅할 수 있다.

Sample TestCase

..
    @Test
    public void testComplexPropertiesOneToManyResultMapSelect()
            throws Exception {
        DeptVO vo = new DeptVO();
        // 20,'RESEARCH','DALLAS'
        vo.setDeptNo(new BigDecimal(20));
 
        // select
        DeptIncludesEmpListVO resultVO =
            empDAO.selectDeptEmpListComplexProperties(
                "selectDeptIncludesEmpListResultUsingResultMap", vo);
 
        // check
        assertNotNull(resultVO);
        assertEquals(new BigDecimal(20), resultVO.getDeptNo());
        assertEquals("RESEARCH", resultVO.getDeptName());
        assertEquals("DALLAS", resultVO.getLoc());
 
        assertTrue(0 < resultVO.getEmpVOList().size());
 
        /*
         * deptNo 20 인 EmpList 는 초기데이터에 따라 7369,'SMITH','CLERK',7902,'1980-12-17',800,NULL,20
         * 7566,'JONES','MANAGER',7839,'1981-04-02',2975,NULL,20 7788,'SCOTT','ANALYST',7566,'1987-04-19',3000,NULL,20
         * 7876,'ADAMS','CLERK',7788,'1987-05-23',1100,NULL,20 7902,'FORD','ANALYST',7566,'1981-12-03',3000,NULL,20
         */
        assertEquals(5, resultVO.getEmpVOList().size());
 
        assertEquals(new BigDecimal(7369), resultVO.getEmpVOList().get(0)
            .getEmpNo());
        assertEquals("SMITH", resultVO.getEmpVOList().get(0).getEmpName());
        assertEquals("CLERK", resultVO.getEmpVOList().get(0).getJob());
        assertEquals(new BigDecimal(7902), resultVO.getEmpVOList().get(0)
            .getMgr());
        SimpleDateFormat sdf =
            new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
        assertEquals(sdf.parse("1980-12-17"), resultVO.getEmpVOList().get(0)
            .getHireDate());
        assertEquals(new BigDecimal(800), resultVO.getEmpVOList().get(0)
            .getSal());
        assertEquals(new BigDecimal(0), resultVO.getEmpVOList().get(0)
            .getComm());
        assertEquals(new BigDecimal(20), resultVO.getEmpVOList().get(0)
            .getDeptNo());
 
        assertEquals(new BigDecimal(7566), resultVO.getEmpVOList().get(1)
            .getEmpNo());
        assertEquals(new BigDecimal(7788), resultVO.getEmpVOList().get(2)
            .getEmpNo());
        assertEquals(new BigDecimal(7876), resultVO.getEmpVOList().get(3)
            .getEmpNo());
        assertEquals(new BigDecimal(7902), resultVO.getEmpVOList().get(4)
            .getEmpNo());
    }

위의 테스트 케이스 검증 코드에서 resultVO.getEmpVOList().get(X).getXXX 와 같이 내포 객체(List<EmpVO>)의 각 EmpVO 와 해당 attributes 가 잘 설정되었음을 확인할 수 있다.

Complext Properties - (1:N 관계 - outer join 의 경우) resultMap

1:N 관계를 포함하는 복합 객체의 리스트를 조회할 때 outer join 을 사용한 예이다.

<sqlMap namespace="EmpComplexResult">
	..
	<select id="selectDeptIncludesEmpListResultListUsingResultMap" parameterClass="deptVO" resultMap="deptIncludesEmpListResult">
		<![CDATA[
			select   A.DEPT_NO as DEPT_NO,
			         DEPT_NAME,
			         LOC,
			         EMP_NO,
			         EMP_NAME,
			         JOB,
			         MGR,
			         HIRE_DATE,
			         SAL,
			         COMM
			from     DEPT A
			         left outer join EMP B
			           on (A.DEPT_NO = B.DEPT_NO)
			where    A.DEPT_NAME like '%'||#deptName#||'%'
			order by A.DEPT_NO,
			         B.EMP_NO
		]]>
	</select>

Sample TestCase

..
    @Test
    public void testComplexPropertiesOneToManyVOListResultMapSelect()
            throws Exception {
        DeptVO vo = new DeptVO();
        // deptName 에 의한 like 검색 테스트 '%'|| 'E' ||'%' --> R'E'S'E'ARCH, SAL'E'S, OP'E'RATIONS
        // 20,'RESEARCH','DALLAS'
        // 30,'SALES','CHICAGO'
        // 40,'OPERATIONS','BOSTON'
        vo.setDeptName("E");
 
        // select
        List<DeptIncludesEmpListVO> resultList =
            empDAO.selectDeptEmpListComplexPropertiesList(isMysql
                ? "selectDeptIncludesEmpListResultListUsingResultMapMysql"
                : "selectDeptIncludesEmpListResultListUsingResultMap", vo);
 
        // check
        assertNotNull(resultList);
        assertEquals(3, resultList.size());
 
        assertEquals(new BigDecimal(20), resultList.get(0).getDeptNo());
        assertEquals(new BigDecimal(30), resultList.get(1).getDeptNo());
        assertEquals(new BigDecimal(40), resultList.get(2).getDeptNo());
 
        /*
         * deptNo 20 인 EmpList 는 초기데이터에 따라 5 명, deptNo 30 인 EmpList 는 초기데이터에 따라 6 명, deptNo 40 인 EmpList 는 초기데이터에 따라 0 명
         * --> cf.)outer join 에 따라 deptNo 만 가진 EmpVO 1건 생김
         */
        assertEquals(5, resultList.get(0).getEmpVOList().size());
        assertEquals(6, resultList.get(1).getEmpVOList().size());
        // cf.)outer join 에 따라 deptNo 만 가진 EmpVO 1건 생김을 확인함. 주의할 것!
        assertEquals(1, resultList.get(2).getEmpVOList().size());
        assertNull(resultList.get(2).getEmpVOList().get(0).getEmpNo());
 
        /*
         * deptNo 20 인 EmpList 는 초기데이터에 따라 7369,'SMITH','CLERK',7902,'1980-12-17',800,NULL,20
         * 7566,'JONES','MANAGER',7839,'1981-04-02',2975,NULL,20 7788,'SCOTT','ANALYST',7566,'1987-04-19',3000,NULL,20
         * 7876,'ADAMS','CLERK',7788,'1987-05-23',1100,NULL,20 7902,'FORD','ANALYST',7566,'1981-12-03',3000,NULL,20
         */
        assertEquals(new BigDecimal(7566), resultList.get(0).getEmpVOList()
            .get(1).getEmpNo());
        assertEquals(new BigDecimal(7788), resultList.get(0).getEmpVOList()
            .get(2).getEmpNo());
        assertEquals(new BigDecimal(7876), resultList.get(0).getEmpVOList()
            .get(3).getEmpNo());
        assertEquals(new BigDecimal(7902), resultList.get(0).getEmpVOList()
            .get(4).getEmpNo());
    }

위의 테스트 케이스 검증 코드에서 iBATIS 의 resultMap 을 사용하여 outer join 의 결과를 복합 객체로 매핑하는 경우 (groupBy 속성 사용 형태) join key 에 해당하는 값만을 가진 하위 객체가 의도하지 않게 생기는 문제가 있어 보이므로 사용에 유의하기 바란다!

Complext Properties - (1:N 관계 - N+1 select 형태) resultMap

EmpVO 의 List 를 1:N 관계의 멤버 attribute 로 포함하고 있는 DeptIncludesEmpListVO 를 결과 객체로 사용하고 있는 것은 같다.

<sqlMap namespace="EmpComplexResult">
	..
	<!-- 1:N 인 경우 N+1 select 형태 - 비추천 -->
	<resultMap id="deptIncludesEmpListUsingSelectAttrResult" class="deptIncludesEmpListVO">
		<result property="deptNo" column="DEPT_NO" />
		<result property="deptName" column="DEPT_NAME" />
		<result property="loc" column="LOC" />
 
		<!-- Dept-EmpList 1:N relation using select attribute -->
		<result property="empVOList" column="DEPT_NO" select="selectEmpList" />
	</resultMap>
 
	<select id="selectDeptIncludesEmpListResultListUsingRepetitionSelect" parameterClass="deptVO"
			resultMap="deptIncludesEmpListUsingSelectAttrResult">
		<![CDATA[
			select   DEPT_NO,
			         DEPT_NAME,
			         LOC
			from     DEPT
			where    DEPT_NAME like '%'||#deptName#||'%'
			order by DEPT_NO
		]]>
	</select>
 
	<select id="selectEmpList" parameterClass="decimal" resultMap="getEmpResult">
		<![CDATA[
			select   EMP_NO,
			         EMP_NAME,
			         JOB,
			         MGR,
			         HIRE_DATE,
			         SAL,
			         COMM,
			         DEPT_NO
			from     EMP
			where    DEPT_NO = #deptNo#
			order by EMP_NO
		]]>
	</select>

위 sql 매핑 파일에서 1:N 관계를 표현하는 Complex Properties 를 포함하는 DeptIncludesEmpListVO 에 대한 resultMap 매핑 처리 시 해당 객체의 empVOList 멤버 attribute 에 대한 매핑 정의를 위해 select=“selectEmpList” 과 같이 별도의 쿼리문을 호출하여(EmpVO 에 대한 resultMap 으로 처리되는) 처리하는 예이다. 쿼리문 상에서 join 을 사용하지 않고 있으며 메인 쿼리문 한번(1번)의 호출에도 결과 rows 수만큼의 select 속성으로 지정한 별도 쿼리 (N번) 가 수행되므로 성능 측면에서 매우 바람직하지 않은 형태이다.

Sample TestCase

..
    @Test
    public void testComplexPropertiesOneToManyVOListRepetitionSelect()
            throws Exception {
        DeptVO vo = new DeptVO();
        // deptName 에 의한 like 검색 테스트 '%'|| 'E' ||'%' --> R'E'S'E'ARCH, SAL'E'S, OP'E'RATIONS
        // 20,'RESEARCH','DALLAS'
        // 30,'SALES','CHICAGO'
        // 40,'OPERATIONS','BOSTON'
        vo.setDeptName("E");
 
        // select
        List<DeptIncludesEmpListVO> resultList =
            empDAO
                .selectDeptEmpListComplexPropertiesList(
                    isMysql
                        ? "selectDeptIncludesEmpListResultListUsingRepetitionSelectMysql"
                        : "selectDeptIncludesEmpListResultListUsingRepetitionSelect",
                    vo);
 
        // check
        assertNotNull(resultList);
        assertEquals(3, resultList.size());
 
        assertEquals(new BigDecimal(20), resultList.get(0).getDeptNo());
        assertEquals(new BigDecimal(30), resultList.get(1).getDeptNo());
        assertEquals(new BigDecimal(40), resultList.get(2).getDeptNo());
 
        /*
         * deptNo 20 인 EmpList 는 초기데이터에 따라 5 명, deptNo 30 인 EmpList 는 초기데이터에 따라 6 명 deptNo 40 인 EmpList 는 초기데이터에 따라 0 명
         * --> 위 outer join 케이스와 달리 EmpList 도 건수 없음 확인
         */
        assertEquals(5, resultList.get(0).getEmpVOList().size());
        assertEquals(6, resultList.get(1).getEmpVOList().size());
        assertEquals(0, resultList.get(2).getEmpVOList().size());
 
        /*
         * deptNo 20 인 EmpList 는 초기데이터에 따라 7369,'SMITH','CLERK',7902,'1980-12-17',800,NULL,20
         * 7566,'JONES','MANAGER',7839,'1981-04-02',2975,NULL,20 7788,'SCOTT','ANALYST',7566,'1987-04-19',3000,NULL,20
         * 7876,'ADAMS','CLERK',7788,'1987-05-23',1100,NULL,20 7902,'FORD','ANALYST',7566,'1981-12-03',3000,NULL,20
         */
        assertEquals(new BigDecimal(7566), resultList.get(0).getEmpVOList()
            .get(1).getEmpNo());
        assertEquals(new BigDecimal(7788), resultList.get(0).getEmpVOList()
            .get(2).getEmpNo());
        assertEquals(new BigDecimal(7876), resultList.get(0).getEmpVOList()
            .get(3).getEmpNo());
        assertEquals(new BigDecimal(7902), resultList.get(0).getEmpVOList()
            .get(4).getEmpNo());
    }

위의 테스트 케이스 검증 코드에서 resultList.get(X).getEmpVOList().get(X).getXXX 와 같이 조회 결과(List) 의 내포 객체(List<EmpVO>)의 각 EmpVO 와 해당 attributes 가 잘 설정되었음을 확인할 수 있다. 그러나 N+1 조회의 성능 문제를 야기하므로 sql 매핑 정의 시 join 쿼리와 groupBy 속성을 통한 1:N 관계 처리 매핑으로 처리하는 것이 바람직하다.

Complext Properties - (Hierarchy 관계) resultMap

..
public class EmpIncludesMgrVO implements Serializable {
 
    private static final long serialVersionUID = 5695339933191681519L;
 
    private BigDecimal empNo;
 
    private String empName;
 
    private String job;
 
    private BigDecimal mgr;
 
    private Date hireDate;
 
    private BigDecimal sal;
 
    private BigDecimal comm;
 
    private BigDecimal deptNo;
 
    // Hierarchy 관계
    private EmpIncludesMgrVO mgrVO;
 
    public BigDecimal getEmpNo() {
        return empNo;
    }
 
    public void setEmpNo(BigDecimal empNo) {
        this.empNo = empNo;
    }
..
    public EmpIncludesMgrVO getMgrVO() {
        return mgrVO;
    }
 
    public void setMgrVO(EmpIncludesMgrVO mgrVO) {
        this.mgrVO = mgrVO;
    }
}

위의 EmpIncludesMgrVO 는 자신과 동일한 EmpIncludesMgrVO 를 Hierarchy 관계의 멤버 attribute 로 포함하고 있다. 위에서는 Emp 의 Manager 에 대해서 포함하고 있는데 MgrVO 를 거슬러 올라가면 결국 자기 관리자 path 에 있는 모든 사원에 대한 정보를 포함하고 있는 객체라 볼 수 있다.

<sqlMap namespace="EmpComplexResult">
	..
	<typeAlias alias="empIncludesMgrVO" type="egovframework.rte.psl.dataaccess.vo.EmpIncludesMgrVO" />
 
	<!-- Hierarchical relation 인 경우 ibatis resultMap select 에 의한 처리 예 -->
	<resultMap id="empIncludesMgrResult" class="empIncludesMgrVO" extends="getEmpResult">
		<result property="mgrVO" column="MGR" select="selectMgrHierarchy" />
	</resultMap>
 
	<!-- 하나의 쿼리를 사용하여 empNo, mgr 속성의 유무에 따라 Hierarchy 반복조회로도 사용토록 변경
		 parameterClass 는 최초 조회와 resultMap 의 select 조회의 경우 empVO 와 decimal 로 다르기 때문에
		 따로 명시하지 않고 자동 reflection 에 의해 처리토록 함
	 -->
	<select id="selectMgrHierarchy" resultMap="empIncludesMgrResult">
		<![CDATA[
			select   EMP_NO,
			         EMP_NAME,
			         JOB,
			         MGR,
			         HIRE_DATE,
			         SAL,
			         COMM,
			         DEPT_NO
			from     EMP
			where    1=1
		]]>
		<!-- 최초 - empNo 는 parameter bean 의 property 임-->
		<isPropertyAvailable property="empNo" prepend="and">
			EMP_NO = #empNo#
		</isPropertyAvailable>
		<!-- 반복 - column="MGR" 에 의한 연결로 empNo 는 property가 아님 -->
		<isNotPropertyAvailable property="empNo" prepend="and">
			EMP_NO = #mgr#
		</isNotPropertyAvailable>
	</select>

위 sql 매핑 파일에서 Hierarchy 관계를 표현하는 Complex Properties 를 포함하는 EmpIncludesMgrVO 에 대한 resultMap 매핑 처리 시 해당 객체의 mgrVO 멤버 attribute 에 대한 매핑 정의를 위해 select=“selectMgrHierarchy” 과 같이 자기 자신의 sql 문을 재호출 하는 형태이다.

Sample TestCase

..
    @Test
    public void testComplexPropertiesHierarcyRepetitionSelect()
            throws Exception {
        EmpVO vo = new EmpVO();
        // 7369,'SMITH','CLERK',7902
        // --> 7902,'FORD','ANALYST',7566
        // --> 7566,'JONES','MANAGER',7839
        // --> 7839,'KING','PRESIDENT',NULL
        vo.setEmpNo(new BigDecimal(7369));
 
        try {
 
            // select
            // EmpIncludesMgrVO resultVO =
            // empDAO.selectEmpMgrHierarchy("selectEmpWithMgr", vo);
            EmpIncludesMgrVO resultVO =
                empDAO.selectEmpMgrHierarchy("selectMgrHierarchy", vo);
 
            // check
            assertNotNull(resultVO);
            assertEquals(new BigDecimal(7369), resultVO.getEmpNo());
            assertEquals("SMITH", resultVO.getEmpName());
            assertEquals("CLERK", resultVO.getJob());
            assertEquals(new BigDecimal(7902), resultVO.getMgr());
            SimpleDateFormat sdf =
                new SimpleDateFormat("yyyy-MM-dd", java.util.Locale
                    .getDefault());
            assertEquals(sdf.parse("1980-12-17"), resultVO.getHireDate());
            assertEquals(new BigDecimal(800), resultVO.getSal());
            assertEquals(new BigDecimal(0), resultVO.getComm());
            assertEquals(new BigDecimal(20), resultVO.getDeptNo());
 
            assertTrue(resultVO.getMgrVO() instanceof EmpIncludesMgrVO);
            assertEquals(new BigDecimal(7902), resultVO.getMgrVO().getEmpNo());
            assertEquals(new BigDecimal(7566), resultVO.getMgrVO().getMgrVO()
                .getEmpNo());
            assertEquals(new BigDecimal(7839), resultVO.getMgrVO().getMgrVO()
                .getMgrVO().getEmpNo());
            assertNull(resultVO.getMgrVO().getMgrVO().getMgrVO().getMgrVO());
 
        } catch (UncategorizedSQLException ue) {
            // tibero 인 경우 ibatis 의 재귀 queyr 형태의 sub 객체 맵핑 시 com.tmax.tibero.jdbc.TbSQLException: TJDBC-90646:Resultset
            // is already closed 에러 발생 확인함!
            assertTrue(isTibero);
            assertTrue(ue.getCause() instanceof NestedSQLException);
            assertTrue(ue.getCause().getCause().getCause().getCause() instanceof TbSQLException);
            assertTrue(((TbSQLException) ue.getCause().getCause().getCause()
                .getCause()).getMessage().contains(
                "TJDBC-90646:Resultset is already closed"));
        }
    }

위의 테스트 케이스 검증 코드에서 SMITH → FORD → JONES → KING 으로 연결되는 관리자 정보를 모두 포함하는 복합 객체의 각 attribute 가 잘 설정되었음을 확인할 수 있다. 일부 DBMS 에서는 문제가 발생할 수 있으므로 사용에 유의한다.

5.9 - iBATIS Dynamic SQL 사용

iBATIS의 Dynamic 요소를 사용하면 다양한 조건에 따라 쿼리를 동적으로 변경할 수 있으며, 조건에 따라 SQL 문의 일부가 추가 또는 제거된다. 예를 들어, 특정 파라미터 값이 있을 때만 조건절이 추가되는 방식으로, SQL의 재사용성을 높이고 복잡한 조건 분기 문제를 해결한다.

Dynamic SQL

일반적으로 JDBC API 를 사용한 코딩에서 한번 정의한 쿼리문을 최대한 재사용하고자 하나 단순 파라메터 변수의 값만 변경하는 것으로 해결하기 어렵고 다양한 조건에 따라 조금씩 다른 쿼리의 실행이 필요한 경우 많은 if~else 조건 분기의 연결이 필요한 문제가 있다. 여기에서는 SQL 문의 동적인 변경에 대한 상대적으로 유연한 방법을 제공하는 iBATIS 의 Dynamic 요소에 대해 알아본다.

기본 Dynamic 요소 사용 방법

아래의 샘플 Dynamic 요소 사용예를 참고하라.

Sample Dynamic SQL mapping xml

	..
	<typeAlias alias="jobHistVO" type="egovframework.rte.psl.dataaccess.vo.JobHistVO" />
 
	<select id="selectJobHistListUsingDynamicElement" parameterClass="jobHistVO" resultClass="jobHistVO">
		<![CDATA[
			select EMP_NO     as empNo,
			       START_DATE as startDate,
			       END_DATE   as endDate,
			       JOB        as job,
			       SAL        as sal,
			       COMM       as comm,
			       DEPT_NO    as deptNo
			from   JOBHIST
		]]>
		<dynamic prepend="where">
			<isNotNull property="empNo" prepend="and">
				EMP_NO = #empNo#
			</isNotNull>		
		</dynamic>
			order by EMP_NO, START_DATE
	</select>

위 sql 매핑 파일에서 파라메터 객체의 empNo 속성의 값 유무에 따라 where EMP_NO = #empNo# 조건절이 동적으로 추가/제거 될 수 있는 예이다. 위에서 dynamic 의 prepend 속성으로 “where” 를 지정하고 있지만 하위 요소의 조건이 하나라도 만족하지 않으면 sql 문에 추가되지 않는다. 또한 위의 예에서는 하위 요소로 isNotNull 태그에 prepend=“and” 가 지정되어 있지만 처음 true 가 되는 조건의 prepend 는 parent 인 dynamic 의 prepend 인 “where” 로 덮어써져 최종적으로는 where EMP_NO = #empNo# 가 됨에 유의한다.

Sample TestCase

..
    @Test
    public void testDynamicStatement() throws Exception {
        JobHistVO vo = new JobHistVO();
        // 입력 파라메터 객체의 property 에 따른 Dynamic 테스트
        vo.setEmpNo(new BigDecimal(7788));
 
        // select
        List<JobHistVO> resultList =
            jobHistDAO.selectJobHistList(
                "selectJobHistListUsingDynamicElement", vo);
 
        // check
        assertNotNull(resultList);
        assertEquals(3, resultList.size());
 
        SimpleDateFormat sdf =
            new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
        assertEquals(sdf.parse("1987-04-19"), resultList.get(0).getStartDate());
        assertEquals(sdf.parse("1988-04-13"), resultList.get(1).getStartDate());
        assertEquals(sdf.parse("1990-05-05"), resultList.get(2).getStartDate());
 
        // 입력 파라메터 객체의 property 에 따른 Dynamic 테스트
        vo.setEmpNo(null);
 
        // select
        resultList =
            jobHistDAO.selectJobHistList(
                "selectJobHistListUsingDynamicElement", vo);
 
        // check
        assertNotNull(resultList);
        // where 이 수행되지 않아 전체 데이터가 조회될 것임
        assertEquals(17, resultList.size());
 
    }

위에서 파라메터 객체에 empNo 의 값을 세팅했을 때 결과로는 해당 조건절이 동적으로 추가된 조회 결과인 사원번호 7788 에 해당하는 Job 이력 3건만 조회되지만 empNo 의 값을 세팅하지 않았을 때(위에서 null 로 세팅하는 것도 동일) 조회 조건절 없이 전체 사원에 대한 이력이 모두 조회됨을 확인할 수 있다.

Unary 조건 비교

아래의 샘플 sql mapping xml 예를 참고하라.

일반적인 경우 Dynamic SQL 작성을 위한 동적 요소는 Where 조건절의 변경을 위해 많이 쓰이지만, 아래에서는 테스트 편의의 목적으로 dual 에 대하여 임의의 상수를 입력 값으로 전달한 결과를 재조회 하는 과정에 Unary 비교 연산을 활용한 예이다.

Sample Unary 비교 연산

	..
	<typeAlias alias="egovMap" type="egovframework.rte.psl.dataaccess.util.EgovMap" />
 
	<select id="selectDynamicUnary" parameterClass="map" remapResults="true" resultClass="egovMap">
		select
		<dynamic>
			<isEmpty property="testEmptyString">
				'empty String' as IS_EMPTY_STRING
			</isEmpty>
			<isNotEmpty property="testEmptyString">
				'not empty String' as IS_EMPTY_STRING
			</isNotEmpty>
			<isEmpty prepend=", " property="testEmptyCollection">
				'empty Collection' as IS_EMPTY_COLLECTION
			</isEmpty>
			<isNotEmpty prepend=", " property="testEmptyCollection">
				'not empty Collection' as IS_EMPTY_COLLECTION
			</isNotEmpty>
			<isNull prepend=", " property="testNull">
				'null' as IS_NULL
			</isNull>
			<isNotNull prepend=", " property="testNull">
				'not null' as IS_NULL
			</isNotNull>
			<isPropertyAvailable prepend=", " property="testProperty">
				'testProperty Available' as	TEST_PROPERTY_AVAILABLE
			</isPropertyAvailable>
			<isNotPropertyAvailable prepend=", " property="testProperty">
				'testProperty Not Available' as TEST_PROPERTY_AVAILABLE
			</isNotPropertyAvailable>
		</dynamic>
		from dual
	</select>

위에서 테스트한 Unary 비교 연산 태그는 다음과 같다.

  • isEmpty : Collection, String(또는 String.valueOf()) 대상 속성이 null 이거나 empty(”” 또는 size() < 1) 인 경우 true
  • isNotEmpty : Collection, String(또는 String.valueOf()) 대상 속성이 not null 이고 not empty(”” 또는 size() < 1) 인 경우 true
  • isNull : 대상 속성이 null 인 경우 true
  • isNotNull : 대상 속성이 not null 인 경우 true
  • isPropertyAvailable : 파라메터 객체에 대상 속성이 존재하는 경우 true
  • isNotPropertyAvailable : 파라메터 객체에 대상 속성이 존재하지 않는 경우 true

Unary 비교 연산 태그에 사용할 수 있는 속성은 다음과 같다.

  • prepend : 동적 구문 앞에 추가되는 override 가능한 SQL 영역.
  • property : 필수. 파라메터 객체의 어떤 property 에 대한 체크인지 지정.
  • removeFirstPrepend : 첫번째로 내포될 내용을 생성하는 태그의 prepend 를 제거할지 여부(true/false)
  • open : 전체 결과 구문에 대한 시작 문자열
  • close : 전체 결과 구문에 대한 닫는 문자열

Sample TestCase

..
    @SuppressWarnings("unchecked")
    @Test
    public void testDynamicUnary() throws Exception {
        Map map = new HashMap();
        // 입력 파라메터 객체의 property 에 따른 Dynamic 테스트
        // isEmpty 테스트 - String
        map.put("testEmptyString", "");
        // isEmpty 테스트 - Collection
        List list = new ArrayList();
        map.put("testEmptyCollection", list);
        // isNull 테스트
        map.put("testNull", null);
        // isPropertyAvailable 테스트 - cf.) property 의 값을 null 로 설정하더라도 해당 property 는 Available 한것에 유의!
        map.put("testProperty", null);
 
        // select
        Map resultMap =
            (Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
                "selectDynamicUnary", map);
 
        // check
        assertNotNull(resultMap);
        assertEquals("empty String", resultMap.get("isEmptyString"));
        assertEquals("empty Collection", resultMap.get("isEmptyCollection"));
        assertEquals("null", resultMap.get("isNull"));
        assertEquals("testProperty Available", resultMap
            .get("testPropertyAvailable"));
 
        // 입력 파라메터 객체의 property 에 따른 Dynamic 테스트 2
        // isEmpty 테스트 - String - null 인 경우도 isEmpty 는 만족함
        map.put("testEmptyString", null);
        // isEmpty 테스트 - Collection - null 인 경우도 isEmpty 는 만족함
        List nullList = null;
        map.put("testEmptyCollection", nullList);
 
        // select
        resultMap =
            (Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
                "selectDynamicUnary", map);
 
        // check
        assertNotNull(resultMap);
        assertEquals("empty String", resultMap.get("isEmptyString"));
        assertEquals("empty Collection", resultMap.get("isEmptyCollection"));
 
        // 입력 파라메터 객체의 property 에 따른 Dynamic 테스트 3
        map.clear();
        // isEmpty 테스트 - String
        map.put("testEmptyString", "aa");
        // isEmpty 테스트 - Collection
        list.clear();
        list.add("aa");
        map.put("testEmptyCollection", list);
        // isNull 테스트
        map.put("testNull", new BigDecimal(0));
        // isPropertyAvailable 테스트 - key 자체를 담지 않았을 때 isNotPropertyAvailable 임
        // map.put("testProperty", null);
 
        // select
        resultMap =
            (Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
                "selectDynamicUnary", map);
 
        // check
        assertNotNull(resultMap);
        assertEquals("not empty String", resultMap.get("isEmptyString"));
        assertEquals("not empty Collection", resultMap.get("isEmptyCollection"));
        assertEquals("not null", resultMap.get("isNull"));
        assertEquals("testProperty Not Available", resultMap
            .get("testPropertyAvailable"));
 
    }

위에서 Unary 조건 비교를 위한 입력 파라메터 객체(여기서는 Map 사용)를 다양하게 세팅하여 어떤 경우에 어떤 조건이 만족하는지 테스트한 예이다. 위에서 isEmpty 의 경우 String 이 null 이거나 ””, Collection 에 하위 element 가 add 되지 않은 경우나 Collection 객체 자체가 null 인 경우에 모두 만족하는 것을 확인할 수 있으며, isPropertyAvailable 태그는 입력 객체에 해당 key 만 추가되있고 값은 null 인 경우에도 true 임을 확인할 수 있다. Dynamic SQL 의 동적인 where 조건절 변경의 경우 전달된 인자의 특정 property 에 대한 isNotNull 또는 isNotEmpty 로 간단히 비교하는 경우가 가장 많이 쓰이게 된다.

Binary 조건 비교

아래의 샘플 sql mapping xml 예를 참고하라.

마찬가지로 아래에서는 테스트 편의의 목적으로 dual 에 대하여 임의의 상수를 입력 값으로 전달한 결과를 재조회 하는 과정에 Binary 비교 연산을 활용한 예이다.

Sample Binary 비교 연산

	..
	<typeAlias alias="egovMap" type="egovframework.rte.psl.dataaccess.util.EgovMap" />
 
	<select id="selectDynamicBinary" parameterClass="map" remapResults="true" resultClass="egovMap">
		select
		<dynamic>
			<isEqual property="testString" compareValue="test">
				'$testString$' as TEST_STRING, 'test : equals' as IS_EQUAL
			</isEqual>
			<isNotEqual property="testString" compareValue="test">
				'$testString$' as TEST_STRING, 'test : not equals' as IS_EQUAL
			</isNotEqual>
			<isPropertyAvailable property="testNumeric">
				<isEqual property="testNumeric" prepend=", " compareValue="10">
					cast($testNumeric$ as $castTypeScale$) as TEST_NUMERIC, '10 : equals' as IS_EQUAL_NUMERIC
				</isEqual>
				<isNotEqual property="testNumeric" prepend=", " compareValue="10">
					cast($testNumeric$ as $castTypeScale$) as TEST_NUMERIC, '10 : not equals' as IS_EQUAL_NUMERIC
				</isNotEqual>
			</isPropertyAvailable>
			<isGreaterEqual property="testNumeric" prepend=", " compareValue="10">
				'10 <![CDATA[<=]]> $testNumeric$' as IS_GREATER_EQUAL
			</isGreaterEqual>
			<isGreaterThan property="testNumeric" prepend=", " compareValue="10">
				'10 <![CDATA[<]]> $testNumeric$' as IS_GREATER_THAN
			</isGreaterThan>
			<isLessEqual property="testNumeric" prepend=", " compareValue="10">
				'10 <![CDATA[>=]]> $testNumeric$' as IS_LESS_EQUAL
			</isLessEqual>
			<isLessThan property="testNumeric" prepend=", " compareValue="10">
				'10 <![CDATA[>]]> $testNumeric$' as IS_LESS_THAN
			</isLessThan>
			<!-- checkMore -->
			<isPropertyAvailable property="testOtherString">
				<isEqual property="testOtherString" prepend=", " compareProperty="testString">
					'$testOtherString$' as TEST_OTHER_STRING, 'test : testOtherString equals testString' as COMPARE_PROPERTY_EQUAL
				</isEqual>
				<isNotEqual property="testOtherString" prepend=", " compareProperty="testString">
					'$testOtherString$' as TEST_OTHER_STRING, 'test : testOtherString not equals testString' as COMPARE_PROPERTY_EQUAL
				</isNotEqual>
				<isGreaterEqual property="testOtherString" prepend=", " compareProperty="testString">
					'''$testOtherString$'' <![CDATA[>=]]> ''$testString$''' as COMPARE_PROPERTY_GREATER_EQUAL
				</isGreaterEqual>
				<isGreaterThan property="testOtherString" prepend=", " compareProperty="testString">
					'''$testOtherString$'' <![CDATA[>]]> ''$testString$''' as COMPARE_PROPERTY_GREATER_THAN
				</isGreaterThan>
				<isLessEqual property="testOtherString" prepend=", " compareProperty="testString">
					'''$testOtherString$'' <![CDATA[<=]]> ''$testString$''' as COMPARE_PROPERTY_LESS_EQUAL
				</isLessEqual>
				<isLessThan property="testOtherString" prepend=", " compareProperty="testString">
					'''$testOtherString$'' <![CDATA[<]]> ''$testString$''' as COMPARE_PROPERTY_LESS_THAN
				</isLessThan>
			</isPropertyAvailable>
		</dynamic>
		from dual
	</select>

위에서 테스트한 Binary 비교 연산 태그는 다음과 같다.

  • isEqual : 대상 속성이 compareValue 값 또는 compareProperty 로 명시한 대상 속성 값과 같은 경우 true
  • isNotEqual : 대상 속성이 compareValue 값 또는 compareProperty 로 명시한 대상 속성 값과 다른 경우 true
  • isGreaterEqual : 대상 속성이 compareValue 값 또는 compareProperty 로 명시한 대상 속성 값보다 크거나 같은 경우 true
  • isGreaterThan : 대상 속성이 compareValue 값 또는 compareProperty 로 명시한 대상 속성 값보다 큰 경우 true
  • isLessEqual : 대상 속성이 compareValue 값 또는 compareProperty 로 명시한 대상 속성 값보다 작거나 같은 경우 true
  • isLessThan : 대상 속성이 compareValue 값 또는 compareProperty 로 명시한 대상 속성 값보다 작은 경우 true

Binary 비교 연산 태그에 사용할 수 있는 속성은 다음과 같다.

  • prepend : 동적 구문 앞에 추가되는 override 가능한 SQL 영역.
  • property : 필수. 파라메터 객체의 어떤 property 에 대한 비교인지 지정.
  • compareProperty : 파라메터 객체의 다른 property 와 대상 property 값을 비교하고자 할 경우 지정. (compareValue 가 없는 경우 필수)
  • compareValue : 대상 property 와 비교될 값을 지정. (compareProperty 가 없는 경우 필수)
  • removeFirstPrepend : 첫번째로 내포될 내용을 생성하는 태그의 prepend 를 제거할지 여부(true/false)
  • open : 전체 결과 구문에 대한 시작 문자열
  • close : 전체 결과 구문에 대한 닫는 문자열

위에서 각 비교 연산 태그의 중첩이 가능함을 확인할 수 있다. 복잡한 조건 처리가 필요한 경우 다양한 비교 연산 태그의 중첩으로 적절히 구성할 수 있을 것이다. 또한 비교 연산자 (>, <) 등과 같이 XML 에서 escape 처리가 필요한 경우 <![CDATA[>]]> 와 같이 CDATA 섹션으로 묶어 사용할 수 있다. CDATA 섹션을 전체 쿼리 영역에 묶어 한번에 사용하면 편하겠지만 Dynamic 요소 자체는 실제 XML 태그로 해석이 되어야 하므로 위와 같이 Dynamic 영역 내에서 발생하는 특수문자에 대해 개별로 사용하는 번거로움이 존재함에 유의한다. cf.) < , > 대신 &lt; , &gt; 와 같이 직접 escape 처리 할수도 있다.

Sample TestCase


..
    @SuppressWarnings("unchecked")
    @Test
    public void testDynamicBinary() throws Exception {
        Map map = new HashMap();
        String castTypeScale = "numeric(2)";
        // oracle 인 경우 - numeric 에 대응되는 type 은 number
        if (isOracle) {
            castTypeScale = "number(2)";
        } else if (isMysql) {
            castTypeScale = "decimal(2)";
        }
 
        // 입력 파라메터 객체의 property 에 따른 Dynamic 테스트
        // isEqual 테스트 - String
        map.put("testString", "test");
        // isEqual 테스트 - BigDecimal
        map.put("testNumeric", new BigDecimal(10));
        // dual 임시 테이블 상에 상수 조회시 numeric(db) - decimal(java) 처리를 위해 cast 처리 추가
        map.put("castTypeScale", castTypeScale);
 
        // select
        Map resultMap =
            (Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
                "selectDynamicBinary", map);
 
        // check
        assertNotNull(resultMap);
        assertEquals("test", resultMap.get("testString"));
        assertEquals("test : equals", resultMap.get("isEqual"));
        assertEquals(new BigDecimal(10), resultMap.get("testNumeric"));
        assertEquals("10 : equals", resultMap.get("isEqualNumeric"));
        assertEquals("10 <= 10", resultMap.get("isGreaterEqual"));
        assertTrue(!resultMap.containsKey("isGreaterThan"));
        assertEquals("10 >= 10", resultMap.get("isLessEqual"));
        assertTrue(!resultMap.containsKey("isLessThan"));
 
        // 입력 파라메터 객체의 property 에 따른 Dynamic 테스트 2
        map.clear();
 
        // isEqual 테스트 - String
        map.put("testString", "not test");
        // isEqual 테스트 - BigDecimal
        map.put("testNumeric", new BigDecimal(11));
        // dual 임시 테이블 상에 상수 조회시 numeric(db) - decimal(java) 처리를 위해 cast 처리 추가
        map.put("castTypeScale", castTypeScale);
 
        // select
        resultMap =
            (Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
                "selectDynamicBinary", map);
 
        // check
        assertNotNull(resultMap);
        assertEquals("not test", resultMap.get("testString"));
        assertEquals("test : not equals", resultMap.get("isEqual"));
        assertEquals(new BigDecimal(11), resultMap.get("testNumeric"));
        assertEquals("10 : not equals", resultMap.get("isEqualNumeric"));
        assertEquals("10 <= 11", resultMap.get("isGreaterEqual"));
        assertEquals("10 < 11", resultMap.get("isGreaterThan"));
        assertTrue(!resultMap.containsKey("isLessEqual"));
        assertTrue(!resultMap.containsKey("isLessThan"));
 
        // 입력 파라메터 객체의 property 에 따른 Dynamic 테스트 2
        map.clear();
 
        // isEqual 테스트 - String
        // isEqual 비교 대상 property 에 null 값을 넘기면 에러는 발생하지 않고, isNotEqual 과 매칭됨
        map.put("testString", null);
        // isEqual 테스트 - BigDecimal
        map.put("testNumeric", new BigDecimal(9));
        // dual 임시 테이블 상에 상수 조회시 numeric(db) - decimal(java) 처리를 위해 cast 처리 추가
        map.put("castTypeScale", castTypeScale);
 
        // select
        resultMap =
            (Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
                "selectDynamicBinary", map);
 
        // check
        assertNotNull(resultMap);
        // oracle 인 경우 '' 는 null 과 같고 결과 객체에는 null 로 맵핑됨
        assertEquals(!(isOracle || isTibero) ? "" : null, resultMap
            .get("testString"));
        assertEquals("test : not equals", resultMap.get("isEqual"));
        assertEquals(new BigDecimal(9), resultMap.get("testNumeric"));
        assertEquals("10 : not equals", resultMap.get("isEqualNumeric"));
        assertTrue(!resultMap.containsKey("isGreaterEqual"));
        assertTrue(!resultMap.containsKey("isGreaterThan"));
        assertEquals("10 >= 9", resultMap.get("isLessEqual"));
        assertEquals("10 > 9", resultMap.get("isLessThan"));
 
        // 입력 파라메터 객체의 property 에 따른 Dynamic 테스트 3
        map.clear();
 
        map.put("testString", "test");
        // isEqual 테스트 - BigDecimal
        map.put("testOtherString", "test");
 
        // select
        resultMap =
            (Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
                "selectDynamicBinary", map);
 
        // check
        assertNotNull(resultMap);
 
        assertEquals("test : equals", resultMap.get("isEqual"));
        // testNumeric property 를 넘기지 않았을 때 기대 결과
        assertTrue(!resultMap.containsKey("isGreaterEqual"));
        assertTrue(!resultMap.containsKey("isGreaterThan"));
        assertTrue(!resultMap.containsKey("isLessEqual"));
        assertTrue(!resultMap.containsKey("isLessThan"));
        // testOtherString 비교
        assertEquals("test", resultMap.get("testOtherString"));
        assertEquals("test : testOtherString equals testString", resultMap
            .get("comparePropertyEqual"));
        assertEquals("'test' >= 'test'", resultMap
            .get("comparePropertyGreaterEqual"));
        assertTrue(!resultMap.containsKey("comparePropertyGreaterThan"));
        assertEquals("'test' <= 'test'", resultMap
            .get("comparePropertyLessEqual"));
        assertTrue(!resultMap.containsKey("comparePropertyLessThan"));
 
        // 입력 파라메터 객체의 property 에 따른 Dynamic 테스트 4
        map.clear();
 
        map.put("testString", "test");
        // 'test' >= 'sample' 테스트
        map.put("testOtherString", "sample");
 
        // select
        resultMap =
            (Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
                "selectDynamicBinary", map);
 
        // check
        assertNotNull(resultMap);
 
        assertEquals("test : equals", resultMap.get("isEqual"));
        // testNumeric property 를 넘기지 않았을 때 기대 결과
        assertTrue(!resultMap.containsKey("isGreaterEqual"));
        assertTrue(!resultMap.containsKey("isGreaterThan"));
        assertTrue(!resultMap.containsKey("isLessEqual"));
        assertTrue(!resultMap.containsKey("isLessThan"));
        // testOtherString 비교
        assertEquals("sample", resultMap.get("testOtherString"));
        assertEquals("test : testOtherString not equals testString", resultMap
            .get("comparePropertyEqual"));
        assertTrue(!resultMap.containsKey("comparePropertyGreaterEqual"));
        assertTrue(!resultMap.containsKey("comparePropertyGreaterThan"));
        assertEquals("'sample' <= 'test'", resultMap
            .get("comparePropertyLessEqual"));
        assertEquals("'sample' < 'test'", resultMap
            .get("comparePropertyLessThan"));
 
        // 입력 파라메터 객체의 property 에 따른 Dynamic 테스트 5
        map.clear();
 
        map.put("testString", "test");
        // 'test' <= 'testa' 테스트
        map.put("testOtherString", "testa");
 
        // select
        resultMap =
            (Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
                "selectDynamicBinary", map);
 
        // check
        assertNotNull(resultMap);
 
        assertEquals("test : equals", resultMap.get("isEqual"));
        // testNumeric property 를 넘기지 않았을 때 기대 결과
        assertTrue(!resultMap.containsKey("isGreaterEqual"));
        assertTrue(!resultMap.containsKey("isGreaterThan"));
        assertTrue(!resultMap.containsKey("isLessEqual"));
        assertTrue(!resultMap.containsKey("isLessThan"));
        // testOtherString 비교
        assertEquals("testa", resultMap.get("testOtherString"));
        assertEquals("test : testOtherString not equals testString", resultMap
            .get("comparePropertyEqual"));
        assertEquals("'testa' >= 'test'", resultMap
            .get("comparePropertyGreaterEqual"));
        assertEquals("'testa' > 'test'", resultMap
            .get("comparePropertyGreaterThan"));
        assertTrue(!resultMap.containsKey("comparePropertyLessEqual"));
        assertTrue(!resultMap.containsKey("comparePropertyLessThan"));
 
    }

위에서 Binary 조건 비교를 위한 입력 파라메터 객체(여기서는 Map 사용)를 다양하게 세팅하여 어떤 경우에 어떤 조건이 만족하는지 테스트한 예이다. 위에서 숫자형의 입력객체 속성에 대해 isGreaterEqual, isGreaterThan, isLessEqual, isLessThan 비교의 경우 쉽게 결과를 예상할 수 있으며 숫자 형식의 결과 조회를 위해 DB 단의 cast 를 처리하게 하였다. 테스트 시나리오 4, 5 에서 String 에 대한 isGreaterEqual, isGreaterThan, isLessEqual, isLessThan 가 가능함을 확인할 수 있으며 ‘sample’ < ’test’ , ’testa’ > ’test’ 임을 확인할 수 있다.

ParameterPresent 비교

아래의 샘플 sql mapping xml 예를 참고하라.

Sample ParameterPresent 비교

	..
	<typeAlias alias="egovMap" type="egovframework.rte.psl.dataaccess.util.EgovMap" />
 
	<select id="selectDynamicParameterPresent" parameterClass="map" remapResults="true" resultClass="egovMap">
		select 
			<isParameterPresent>
				'parameter object exist' as IS_PARAMETER_PRESENT
			</isParameterPresent>
			<isNotParameterPresent>
				'parameter object not exist' as IS_PARAMETER_PRESENT
			</isNotParameterPresent>
		from dual
	</select>
  • isParameterPresent : 파라메터 객체가 전달된(not null) 경우 true
  • isNotParameterPresent : 파라메터 객체가 전달되지 않은(null) 경우 true

ParameterPresent 비교 연산 태그에 사용할 수 있는 속성은 다음과 같다.

  • prepend : 동적 구문 앞에 추가되는 override 가능한 SQL 영역.
  • removeFirstPrepend : 첫번째로 내포될 내용을 생성하는 태그의 prepend 를 제거할지 여부(true/false)
  • open : 전체 결과 구문에 대한 시작 문자열
  • close : 전체 결과 구문에 대한 닫는 문자열

Sample TestCase

..
    @SuppressWarnings("unchecked")
    @Test
    public void testDynamicParameterPresent() throws Exception {
 
        // 입력 파라메터 객체의 전달 여부에 따른 Dynamic 테스트
        // isParameterPresent 테스트
        Map map = new HashMap();
 
        // select
        Map resultMap =
            (Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
                "selectDynamicParameterPresent", map);
 
        // check
        assertNotNull(resultMap);
        assertEquals("parameter object exist", resultMap
            .get("isParameterPresent"));
 
        map = null;
 
        // select
        resultMap =
            (Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
                "selectDynamicParameterPresent", map);
 
        // check
        assertNotNull(resultMap);
        assertEquals("parameter object not exist", resultMap
            .get("isParameterPresent"));
    }

iBATIS 의 쿼리문 실행을 위한 API 호출 시 파라메터 객체의 전달 여부에 따라 isParameterPresent, isNotParameterPresent 의 비교 연산을 사용할 수 있다.

iterate 연산

아래의 샘플 sql mapping xml 예를 참고하라.

일반적으로 iterate 태그 처리에 가장 많이 사용되는 in 조건절 처리 예이다.

Sample iterate 연산

	..
 	<typeAlias alias="jobHistVO" type="egovframework.rte.psl.dataaccess.vo.JobHistVO" />
	<typeAlias alias="empIncludesEmpListVO" type="egovframework.rte.psl.dataaccess.vo.EmpIncludesEmpListVO" />
 
	<select id="selectJobHistListUsingDynamicIterate" parameterClass="empIncludesEmpListVO" resultClass="jobHistVO">
		<![CDATA[
			select EMP_NO     as empNo,
			       START_DATE as startDate,
			       END_DATE   as endDate,
			       JOB        as job,
			       SAL        as sal,
			       COMM       as comm,
			       DEPT_NO    as deptNo
			from   JOBHIST
		]]>
		<dynamic prepend="where">
			<iterate property="empList" open="EMP_NO in (" conjunction=", " close=")">
				#empList[].empNo#
			</iterate>		
		</dynamic>
			order by EMP_NO, START_DATE
	</select>
  • iterate : collection 형태의 대상 객체에 대하여 포함하고 있는 각 개별 요소만큼 반복 루프를 돌며 해당 내용을 수행한다.

iterate 태그에 사용할 수 있는 속성은 다음과 같다.

  • prepend : 동적 구문 앞에 추가되는 override 가능한 SQL 영역.
  • property : java.util.Collection 이나 java.util.Iterator 또는 array(배열) 유형인 대상 객체를 지정. 명시되지 않으면 파라메터 객체가 collection 임을 가정하여 처리됨.
  • removeFirstPrepend : 첫번째로 내포될 내용을 생성하는 태그의 prepend 를 제거할지 여부(true/false/iterate)
  • open : 전체 결과 구문에 대한 시작 문자열. 괄호 용도로 유용함.
  • close : 전체 결과 구문에 대한 닫는 문자열. 괄호 용도로 유용함.
  • conjunction : 각 iteration 사이에 적용될 문자열. AND, OR 연산자나 ‘,’ 등의 구분자 필요시 유용함.

위에서는 empList 라는 attribute 로 List<EmpVO> 인 리스트 객체를 포함하는 EmpIncludesEmpListVO 가 파라메터 객체로 사용되고 있으며, iterate 태그에 의해서 empList 의 size 만큼 각 EmpVO 객체의 empNo 값이 in 리스트에 포함되는 아래의 조건절이 동적으로 만들어지게 된다.

where EMP_NO in ( ? , ? , ? ) 

iterate 태그의 body 영역 표기법에 유의한다. #empList[].empNo# 에서 확인할 수 있듯이 looping 중의 현재 item 을 지시하기 위해 ‘리스트속성명[]’ 이 사용되고 있으며 여기서는 해당 item 이 EmpVO 이고 empNo 라는 property 를 포함하고 있으며 이 값이 in 절의 현재 항목으로 바인딩되고 있다. 만약 파라메터 객체 자체가 iterate 가능한 형태인 경우 iterate property 의 명시 없이 body 영역에 ‘[]’ 로 바로 현재 item 을 지시할 수 있다.

  • iBATIS 에서는 파라메터 객체가 복합 객체인 경우 dot notation (. 을 사용하여) 을 사용하여 쉽게 원하는 sub 객체/속성에 접근할 수 있다.

Sample TestCase

..
    @SuppressWarnings("unchecked")
    @Test
    public void testDynamicIterate() throws Exception {
        // CompositeKeyTest.testCompositeKeySelect() 참조
        EmpVO vo = new EmpVO();
        // 7521,'WARD','SALESMAN',7698,'1981-02-22',1250,500,30
        // --> mgr 이 7698 인 EMP
        // 7499,'ALLEN','SALESMAN',7698,'1981-02-20',1600 --> O
        // 7654,'MARTIN','SALESMAN',7698,'1981-09-28',1250 --> O
        // 7844,'TURNER','SALESMAN',7698,'1981-09-08',1500 --> O
        // 7900,'JAMES','CLERK',7698,'1981-12-03',950 --> X
        vo.setEmpNo(new BigDecimal(7521));
 
        // select
        EmpIncludesEmpListVO resultVO =
            empDAO.selectEmpIncludesEmpList(
                "selectEmpIncludesSameMgrMoreSalaryEmpList", vo);
 
        // check
        assertNotNull(resultVO);
        assertEquals(new BigDecimal(7521), resultVO.getEmpNo());
        assertEquals("WARD", resultVO.getEmpName());
        assertTrue(resultVO.getEmpList() instanceof List);
        assertEquals(3, resultVO.getEmpList().size());
        assertEquals(new BigDecimal(7499), resultVO.getEmpList().get(0)
            .getEmpNo());
        assertEquals(new BigDecimal(1600), resultVO.getEmpList().get(0)
            .getSal());
        assertEquals(new BigDecimal(7844), resultVO.getEmpList().get(1)
            .getEmpNo());
        assertEquals(new BigDecimal(1500), resultVO.getEmpList().get(1)
            .getSal());
        assertEquals(new BigDecimal(7654), resultVO.getEmpList().get(2)
            .getEmpNo());
        assertEquals(new BigDecimal(1250), resultVO.getEmpList().get(2)
            .getSal());
 
        // select
        List<JobHistVO> resultList =
            jobHistDAO.getSqlMapClientTemplate().queryForList(
                "selectJobHistListUsingDynamicIterate", resultVO);
 
        assertNotNull(resultList);
        // 7499, 7654, 7844 의 jobhist 는 초기데이터에 따라 각 1건 임
        assertEquals(3, resultList.size());
 
        assertEquals(new BigDecimal(7499), resultList.get(0).getEmpNo());
        assertEquals(new BigDecimal(7654), resultList.get(1).getEmpNo());
        assertEquals(new BigDecimal(7844), resultList.get(2).getEmpNo());
 
        SimpleDateFormat sdf =
            new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
        assertEquals(sdf.parse("1981-02-20"), resultList.get(0).getStartDate());
        assertEquals(sdf.parse("1981-09-28"), resultList.get(1).getStartDate());
        assertEquals(sdf.parse("1981-09-08"), resultList.get(2).getStartDate());
 
    }

위에서 이전에 작성한 CompositeKeyTest 의 쿼리를 실행하여 리스트 형태의 속성을 가지는 복합 객체를 만들었고, 이를 파라메터 객체로 iterate 테스트를 위한 쿼리를 수행하였다.

iterate property 가 파라메터 객체의 속성 vs. 파라메터 객체 자신일 때 비교

	..
 
	<!-- parameterClass 는 명시하지 않았음. Map 에 collection 이란 key 로 List 를 넘긴 경우와 바로 List를 넘긴 경우로 구분하여 테스트 -->
	<!-- iterate 요소가 검색조건 등의 입력 파라메터 바인딩 변수로 사용될 경우는 #collection[]# 과 같이 사용하면 됨 -->
	<select id="selectDynamicIterateSimple" resultClass="egovMap">
		select 
			<isPropertyAvailable property="collection">
				<iterate property="collection" conjunction=", ">
					'$collection[]$' as $collection[]$
				</iterate>
			</isPropertyAvailable>
			<!-- List 를 바로 넘긴 경우 -->
			<isNotPropertyAvailable property="collection">
				<iterate conjunction=", ">
					'$[]$' as $[]$
				</iterate>
			</isNotPropertyAvailable>
		from dual
	</select>

collection 이란 속성이 포함됬는지 여부에 따라 iterate 대상이 파라메터 객체의 속성(위에서는 파라메터 객체 내에 collection 이라는 property 로 전달된 리스트) 또는 파라메터 객체 자신(파라메터 객체 자신이 리스트 형태인 경우) 에 대한 iterate 처리 예이다. $property명$ 로 작성된 영역은 #property명# 와 같이 prepared statement 의 바인드 변수로 처리되는 것이 아니라 SQL 문 자체에 텍스트가 replace 되어 처리됨에 유의한다.

Sample TestCase

..
    @SuppressWarnings("unchecked")
    @Test
    public void testDynamicIterateSimple() throws Exception {
        // Collection 형의 객체 size 만큼
        List iterateList = new ArrayList();
        iterateList.add("a");
        iterateList.add("b");
        iterateList.add("c");
 
        // select
        Map resultMap =
            (Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
                "selectDynamicIterateSimple", iterateList);
 
        // check
        assertNotNull(resultMap);
        assertEquals("a", resultMap.get("a"));
        assertEquals("b", resultMap.get("b"));
        assertEquals("c", resultMap.get("c"));
        assertTrue(!resultMap.containsKey("d"));
 
        // map 안에 collection 이란 property 로 List 를 넣은 경우
        Map map = new HashMap();
        map.put("collection", iterateList);
 
        // select
        resultMap =
            (Map) jobHistDAO.getSqlMapClientTemplate().queryForObject(
                "selectDynamicIterateSimple", map);
 
        // check
        assertNotNull(resultMap);
        assertEquals("a", resultMap.get("a"));
        assertEquals("b", resultMap.get("b"));
        assertEquals("c", resultMap.get("c"));
        assertTrue(!resultMap.containsKey("d"));
 
        // iterate 를 위한 테스트로 Map, Set, Iterator 를 시도해 보았으나 아래 에러를 냄. (List 나 Array 와 같이 index 로 접근 가능해야 하는듯)
        // The 'xxx'(ex. collection) property of the XXX (ex. java.util.HashMap$EntryIterator) class is not a List or
        // Array.
    }

위에서 파라메터 객체 자체를 List 로 전달한 첫번째 경우와 파라메터 객체(Map) 안에 “collection” 이란 key 로 List 를 전달한 두번째 경우의 iterate 태그 처리 차이를 알 수 있을 것이다.

Nested iterate 연산

아래의 샘플 sql mapping xml 예를 참고하라.

Sample iterate 연산

	..
 
	<select id="selectJobHistListUsingDynamicNestedIterate" parameterClass="map" resultClass="jobHistVO">
		<![CDATA[
			select EMP_NO     as empNo,
			       START_DATE as startDate,
			       END_DATE   as endDate,
			       JOB        as job,
			       SAL        as sal,
			       COMM       as comm,
			       DEPT_NO    as deptNo
			from   JOBHIST
		]]>
		<dynamic prepend="where">
			<iterate property="condition" open="(" conjunction="and" close=")">
				$condition[].columnName$ $condition[].columnOperation$ 
				<isEqual property="condition[].nested" compareValue="true">
					<iterate property="condition[].columnValue" open="(" conjunction="," close=")">
						#condition[].columnValue[]#
					</iterate>
				</isEqual>
				<isNotEqual property="condition[].nested" compareValue="true">
					#condition[].columnValue#
				</isNotEqual>
			</iterate>		
		</dynamic>
			order by EMP_NO, START_DATE
	</select>

복잡한 조건 처리의 예로 일반 비교 연산과 Nested Iterate 처리가 함께 사용되고 있다. 쿼리 호출 시 columnName, columnOperation, columnValue 를 멀티로 넘기며 columnName 과 columnOperation 은 sql 문에 직접 replaced Text 로 처리하고 columnValue 에 대해서는 바인드 변수 처리하며, 이때 nested 로 추가 설정한 값이 true 이면 columnValue 가 in 조건절인 경우로 판단하여 nested iterate 처리하는 예이다. 아래 테스트 케이스에서 파라메터 객체 세팅에 따라 다음 조건절이 동적으로 추가된다.

where ( DEPT_NO = ? and SAL < ? and JOB in ( ? , ? ) )

Sample TestCase

..
    @SuppressWarnings("unchecked")
    @Test
    public void testDynamicNestedIterate() throws Exception {
        // nested iterate 태그 테스트 - columnName, columnOperation, columnValue 를 Map 형태로 모아 담은 List 를 condition 이란 key 로 파라메터 객체(Map) 에 추가
        // columnValue 가 nested iterate 로 풀려야 하는 경우(ex. in 조건절) nested 'true' 로 추가 설정을 하여 호출함.
        Map map = new HashMap();
        List condition = new ArrayList();
        Map columnMap1 = new HashMap();
        columnMap1.put("columnName", "DEPT_NO");
        columnMap1.put("columnOperation", "=");
        columnMap1.put("columnValue", new BigDecimal(30));
        condition.add(columnMap1);
 
        Map columnMap2 = new HashMap();
        columnMap2.put("columnName", "SAL");
        columnMap2.put("columnOperation", "<");
        columnMap2.put("columnValue", new BigDecimal(3000));
        condition.add(columnMap2);
 
        Map columnMap3 = new HashMap();
        columnMap3.put("columnName", "JOB");
        columnMap3.put("columnOperation", "in");
        List jobList = new ArrayList();
        jobList.add("CLERK");
        jobList.add("SALESMAN");
        columnMap3.put("columnValue", jobList);
        // List 를 nested 로 포함하고 있음을 flag 로 알림
        columnMap3.put("nested", "true");
        condition.add(columnMap3);
 
        map.put("condition", condition);
 
        // select
        List<JobHistVO> resultList =
            jobHistDAO.getSqlMapClientTemplate().queryForList(
                "selectJobHistListUsingDynamicNestedIterate", map);
 
        // check
        assertNotNull(resultList);
 
        // 결과 데이터
        // Empno Startdate Enddate Job Sal Comm Deptno
        // 1 7499 81/02/20 SALESMAN 1600 300 30
        // 2 7521 81/02/22 SALESMAN 1250 500 30
        // 3 7654 81/09/28 SALESMAN 1250 1400 30
        // cf.) 7698 81/05/01 MANAGER 2850 30 데이터는 in 조건절에 JOB 이 'MANAGER' 인 것이 없기 때문에 nested 안에서 필터링 됨.
        // 4 7844 81/09/08 SALESMAN 1500 0 30
        // 5 7900 83/01/15 CLERK 950 30
        assertEquals(5, resultList.size());
        assertEquals(new BigDecimal(7499), resultList.get(0).getEmpNo());
        assertEquals(new BigDecimal(7521), resultList.get(1).getEmpNo());
        assertEquals(new BigDecimal(7654), resultList.get(2).getEmpNo());
        assertEquals(new BigDecimal(7844), resultList.get(3).getEmpNo());
        assertEquals(new BigDecimal(7900), resultList.get(4).getEmpNo());
 
        SimpleDateFormat sdf =
            new SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault());
        assertEquals(sdf.parse("1981-02-20"), resultList.get(0).getStartDate());
        assertEquals(sdf.parse("1981-02-22"), resultList.get(1).getStartDate());
        assertEquals(sdf.parse("1981-09-28"), resultList.get(2).getStartDate());
        assertEquals(sdf.parse("1981-09-08"), resultList.get(3).getStartDate());
        assertEquals(sdf.parse("1983-01-15"), resultList.get(4).getStartDate());
 
    }

지금까지 살펴본 바에서 확인할 수 있듯이 iBATIS 의 Dynamic 요소를 사용하여 매우 복잡한 조건 처리도 가능하다. 그러나 조건 처리가 복잡한 경우 dynamic 태그 영역을 쉽게 알아보기 어렵고 단순 논리/산술 연산 수준의 태그로 감당하기 어려운 복잡한 요구사항에 완벽하게 대응하기는 미비한 점이 존재한다. iBATIS 차후 버전에서는 좀더 유연하고 강력한 Dynamic 처리가 가능해질 걸로 보인다.

5.11 - MyBatis 주요 변경점

MyBatis는 iBatis에서 패키지 구조와 용어가 변경되었으며, com.ibatis.*에서 org.apache.ibatis.*로 패키지 구조가 바뀌고, 여러 구문 및 속성들이 통합 또는 대체되었다. 예를 들어, sqlMap은 mapper로, parameterClass는 parameterType으로 변경되었다.

MyBatis 주요 변경점

본 가이드는 MyBatis와 iBatis의 차이점을 설명한다.

변경된 용어(종합)

iBatisMyBatis비고
com.ibatis.*org.apache.ibatis.*패키지 구조 변경
SqlMapConfigConfigration용어변경
sqlMapmapper용어변경
sqlMapClientsqlSession구문대체
rowHandlerresultHandler구문대체
resultHandlerSqlSessionFactory구문대체
parameterMap, parameterClassparameterType속성 통합
resultClassresultType용어변경
#var##{var}구문대체
$var$${var}구문대체
, 구문대체

변경사항

패키지 구조 변경

iBatisMyBatis
com.ibatis.*org.apache.ibatis.*

패키지 구조는 변경되었으나 기존에 iBatis 패키지명은 그대로 사용한다.

MyBatis library 별도 제공

Maven Dependency Information 예시

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.2.2</version>
</dependency>

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.2.0</version>
</dependency>

annotation 도입

  • annotation 을 적극 도입하여 DAO 에 대해서 행하던 sqlMapClient DI 설정을 안 해도 된다.
  • spring 2.5 대 부터 annotation 이 도입되어서 설정이 매우 간편해진 것처럼 무척 간편해 졌다.
  • bean id sqlSessionFactory, sqlSessionTemplate 만 지정하면 된다.

rowHandler 대체

  • xml 및 대량 데이터 처리를 위해 사용하였던 rowHandler가 삭제되었다.
  • sqlMapClient 가 없어지고 sqlSession 으로 대체 되었는데, sqlSession 의 API 를 살펴보니 large data 처리용 method 를 제공한다.
  • rowHandler 가 resultHandler 로 바뀌었다.
  • 큰 변화중 하나는 자바 어노테이션을 사용해서 xml을 사용하지 않고 모든 것을 자바로만 할 수 있게 되었다. 물론 Configration.xml 도 자바에서 직접 DataSource, Environment 등을 선언해서 클래스화 시킬 수 있다.
  • 주의할 점은 xml로 Configure를 만들고 환경변수와 property를 클래스로도 만들었다면, 클래스 쪽이 나중에 읽어지게 돼서 xml로 되어있는 세팅이 자바 클래스에서 선언해 놓은 것으로 덮어써지게 된다. 혼란을 줄 수 있으니 한가지 방법만으로 프로젝트를 구성하는 것이 좋을 것이다.
  • Configuration configuration = new Con…. 형식으로 선언을 하고 나서는 mapper도 xml이 아니고 configuration.addMapper(UserMapper.class) 형식으로 추가 해야 하기 때문에 어느 쪽으로 할 것인지 확실하게 결정을 하고 나서 진행해야 한다.

네임스페이스 방식 변경

  • sqlMap 파일별로 줄여 놓은 이름을 사용했다면 이제 전체 경로(full path)로 사용하게 된다.(공식 설명서에서는 혼란을 줄이고 어떤 것이 호출되는지 정확하게 알 수 있으니 좋다고 기재)
iBatisMyBatis
<sqlMap namespace=“User”><mapper namespace=“myBatis.mapper.UserMapper”>

실제 자바쪽에서 호출할 때도 길게 호출하여야 한다.

list = session.selectList("myBatis.mappers.UserMapper.getUserList");

이런 경우에 위에서 이야기한 자바 어노테이션 (@Select)을 사용해서 mapper 파일을 xml이 아니고 자바로 만들어 놓으면 코드 힌트까지 사용해서 편하게 쓸 수 있다.

UserMapper mapper = session.getMapper(UserMapper.class);

list = mapper.selectUserList();

변경되거나 추가된 속성들

기존에 조건에 따라 변하는 쿼리를 만들기 위해서 사용되던 태그들이 변경되었다. 조금 더 직관적으로 바뀌었고 해당상황(Update, Select)등에 맞춰서 사용할 수 있는 태그들도 추가되었다.

  • parameterMap은 더이상 사용하지 않게 되었다. parameterMap과 parameterClass 대신 parameterType 하나로 사용한다.
  • resultMap은 여전히 남아있지만 resultClass 는 resultType 으로 변경되었다.
  • parameterType과 resultType에는 기본형(int, byte, …. )부터 클래스 명까지 기존처럼 사용할 수 있다.
  • 기존에 procedure를 호출하기 위해 사용하던 <procedure>가 사라지고 statementType 속성이 생겼다. PREPARED, STATEMENT, CALLABLE 중에 하나를 선택할 수 있고 기본값은 PREPARED이다.
  • 파라미터를 매핑하기 위해서 사용하던 #var# 형태는 #{var} 로 바뀌었다. $var$ 역시 ${var} 형태로 사용하면 된다.

참고) #{var}와 ${var}의 차이는 prepredStatement의 파라미터로 사용, String 값으로 사용.
order by 같은 경우에 사용하기 위해서는 order by ${orderParam} 처럼 사용해야 한다.
이 방법을 사용하는 경우 MyBatis가 자체적으로 쿼리의 적합성여부를 판단할 수 없기 때문에 사용자의 입력 값을 그대로 사용하는 것보다는 개발자가 미리 정해 놓은 값 등으로 변경하도록 해서 정확한 값이 들어올 수 있도록 해야 한다.

sqlMap쪽에서 사용하던 typeAlias가 sqlMap이 바뀐 mapper 에서 사용되지 않고 Configration 파일에서 정의하도록 변경되었다.

<typeAliases>
    <typeAlias type="vo.UserVO" alias="User"/>
</typeAliases>

Configuration 파일에 위의 형식처럼 Alias를 정의하면 전체 mapper 에서 사용할 수 있다.

Dynamic Statement의 변화

  • <isEqual> , <isNull> 등의 구문이 <if>로 통합되었다.

<if test=“userID != null”> 형태로 간단하게 사용할 수 있다.
<dynamic> 형태로 해서 where 조건절이나 and , or 를 동적으로 만들던 것이 <where>나 update에서 사용할 수 있는 <set> 등으로 변경되었다.

<select id="getUserList" resultType="User>
    SELECT * FROM TR_USER
        <where>
            <if test="isAdmin != null">
                authLevel = '1'
             </if>
          </where>
</select>
  • trim, foreach 태그가 새로 추가
  1. trim은 쿼리를 동적 생성 할 때에 쿼리를 연결하기 위해서 컴마(,)를 사용한 경우 마지막항목이 조건을 만족하지 못해서 생성된 쿼리 끝에 컴마가 붙어있다던가 하는 경우에 잘라낼 수 있다.
  2. foreach는 반복적인 항목을 동적으로 넣을 때 사용할 수 있다. ( ex. where 조건절에서 in 을 사용하는 경우)

참고자료

5.12 - MyBatis 시작하기

MyBatis 애플리케이션은 SqlSessionFactory 인스턴스를 사용하며, 이를 SqlSessionFactoryBuilder를 통해 XML 설정 파일에서 빌드할 수 있다. Resources 유틸 클래스를 사용하여 클래스패스 또는 다른 위치에서 자원을 쉽게 로드할 수 있다.

시작하기

모든 MyBatis 애플리케이션은 SqlSessionFactory 인스턴스를 사용한다. SqlSessionFactory 인스턴스는 SqlSessionFactoryBuilder 를 사용하여 만들 수 있다. SqlSessionFactoryBuilder 는 XML 설정파일에서 SqlSessionFactory 인스턴스를 빌드할 수 있다;

XML에서 SqlSessionFactory 빌드하기

XML 파일에서 SqlSessionFactory 인스턴스를 빌드하는 것은 매우 간단하다. 설정을 위해 클래스패스 자원을 사용하는 것을 추천하나, 파일 경로나 file URL 로부터 만들어진 InputStream 인스턴스를 사용할 수도 있다. MyBatis 는 클래스패스와 다른 위치에서 자원을 로드하는 것으로 좀더 쉽게 해주는 Resources 라는 유틸성 클래스를 가지고 있다.

String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

XML 설정파일에서 지정하는 MyBatis 의 핵심이 되는 설정은 트랜잭션을 제어하기 위한 TransactionManager 과 함께 데이터베이스 Connection 인스턴스를 가져오기 위한 DataSource 를 포함한다. 예제 :

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${driver}"/>
        <property name="url" value="${url}"/>
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/mybatis/example/BlogMapper.xml"/>
  </mappers>
</configuration>

좀 더 많은 XML 설정방법이 있지만, 위 예제에서는 가장 핵심적인 부분만을 보여주고 있다. XML 가장 위부분에서는 XML 문서의 유효성체크를 위해 필요하다. environment 요소는 트랜잭션 관리와 커넥션 풀링을 위한 환경적인 설정을 나타낸다. mappers 요소는 SQL 코드와 매핑 정의를 가지는 XML 파일인 mapper 의 목록을 지정한다.

XML 을 사용하지 않고 SqlSessionFactory 빌드하기

XML 보다 자바를 사용해서 직접 설정하길 원한다면, XML 파일과 같은 모든 설정을 제공하는 Configuration 클래스를 사용하면 된다.

DataSource dataSource = DeptDataSourceFactory.getDeptDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(DeptMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);

이 설정에서 추가로 해야 할 일은 Mapper 클래스를 추가하는 것이다. Mapper 클래스는 SQL 매핑 어노테이션을 가진 자바 클래스이다. 어쨌든 자바 어노테이션의 몇 가지 제약과 몇 가지 설정방법의 복잡함에도 불구하고, XML 매핑은 세부적인 매핑을 위해 언제든 필요하다.

SqlSessionFactory 에서 SqlSession 만들기

SqlSessionFactory 이름에서 보듯이, SqlSession 인스턴스를 만들 수 있다. SqlSession 은 데이터베이스에 대해 SQL 명령어를 실행하기 위해 필요한 모든 메서드를 가지고 있다. 그래서 SqlSession 인스턴스를 통해 직접 SQL 구문을 실행할 수 있다. 예를 들면 :

 SqlSession session = sqlSessionFactory.openSession();
 try {
   Dept dept = session.selectOne("egovframework.rte.psl.dataaccess.DeptMapper.selectDept", 101);
 } finally {
   session.close();
 }

이 방법이 MyBatis 의 이전버전을 사용한 사람이라면 굉장히 친숙할 것이다. 하지만 좀더 좋은 방법이 생겼다. 주어진 SQL 구문의 파라미터와 리턴값을 설명하는 인터페이스(예를 들면, DeptMapper.class )를 사용하여, 문자열 처리 오류나 타입 캐스팅 오류 없이 좀더 타입에 안전하고 깔끔하게 실행할 수 있다.

예를 들면:

SqlSession session = sqlSessionFactory.openSession();
try {
  DeptMapper mapper = session.getMapper(DeptMapper.class);
  Dept dept = mapper.selectDept(101);
} finally {
  session.close();
}

그럼 좀더 자세히 살펴보자.

매핑된 SQL 구문 살펴보기

이 시점에 SqlSession 이나 Mapper 클래스가 정확히 어떻게 실행되는지 궁금할 것이다. 매핑된 SQL 구문에 대한 내용이 가장 중요하다. 그래서 이 문서 전반에서 가장 자주 다루어진다. 하지만 다음의 두가지 예제를 통해 정확히 어떻게 작동하는지에 대해서는 충분히 이해하게 될 것이다.

위 예제처럼, 구문은 XML 이나 어노테이션을 사용해서 정의할 수 있다. 그럼 먼저 XML 을 보자. MyBatis 가 제공하는 대부분의 기능은 XML 을 통해 매핑 기법을 사용한다. 이전에 MyBatis 를 사용했었다면 쉽게 이해되겠지만, XML 매핑 문서에 이전보다 많은 기능이 추가되었다. SqlSession 을 호출하는 XML 기반의 매핑 구문이다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper   PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
     "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="egovframework.rte.psl.dataaccess.DeptMapper">
	<select id="selectDept" 
                parameterType="int" 
                     resultType="Dept">
		<![CDATA[
			select *
			from   DEPT
			where  DEPT_NO = #{deptNo}
		]]>
	</select>
</mapper>

한 개의 매퍼 XML 파일에는 많은 수의 매핑 구문을 정의할 수 있다. XML 도입부의 헤더와 doctype 을 제외하면, 나머지는 쉽게 이해되는 구문의 형태이다. 여기선 egovframework.rte.psl.dataaccess.DeptMapper 명명공간에서 selectDept 라는 매핑 구문을 정의했고, 이는 결과적으로 egovframework.rte.psl.dataaccess.DeptMapper.selectDept 형태로 실제 명시되게 된다. 그래서 다음처럼 사용하게 되는 셈이다.

Dept dept = (Dept) session.selectOne(“egovframework.rte.psl.dataaccess.DeptMapper.selectDept”, 101); 이건 마치 패키지를 포함한 전체 경로의 클래스내 메서드를 호출하는 것과 비슷한 형태이다. 이 이름은 매핑된 select 구문의 이름과 파라미터 그리고 리턴 타입을 가진 명명공간과 같은 이름의 Mapper 클래스와 직접 매핑될 수 있다. 이건 위에서 본 것과 같은 Mapper 인터페이스의 메서드를 간단히 호출하도록 허용한다. 위 예제에 대응되는 형태는 아래와 같다.

DeptMapper mapper = session.getMapper(DeptMapper.class); Dept dept = mapper.selectDept(101); 두번째 방법은 많은 장점을 가진다. 먼저 문자열에 의존하지 않는다는 것이다. 이는 애플리케이션을 좀더 안전하게 만든다. 두번째는 개발자가 IDE 를 사용할 때, 매핑된 SQL 구문을 사용할 때의 수고를 덜어준다. 세번째는 리턴 타입에 대해 타입 캐스팅을 하지 않아도 된다. 그래서 DeptMapper 인터페이스는 깔끔하고 리턴 타입에 대해 타입에 안전하며 이는 파라미터에도 그대로 적용된다.

참고 : 명명공간(Namespaces)에 대한 설명

명명공간(Namespaces) 이 이전 버전에서는 사실 선택 사항이었다. 하지만 이제는 패키지 경로를 포함한 전체 이름을 가진 구문을 구분하기 위해 필수로 사용해야 한다.

명명공간은 인터페이스 바인딩을 가능하게 한다. 명명공간을 사용하고 자바 패키지의 명명공간을 두면 코드가 깔끔해 지고 MyBatis 의 사용성이 크게 향상될 것이다.

이름 분석(Name Resolution): 타이핑을 줄이기 위해, MyBatis 는 구문과 결과맵, 캐시등의 모든 설정요소를 위한 이름 분석 규칙을 사용한다.

  • “com.mypackage.MyMapper.selectAllThings” 과 같은 패키지를 포함한 전체 경로명(Fully qualified names)은 같은 형태의 경로가 있다면 그 경로내에서 직접 찾는다.
  • “selectAllThings” 과 같은 짧은 형태의 이름은 모호하지 않은 엔트리를 참고하기 위해 사용될 수 있다. 그래서 짧은 이름은 모호해서 에러를 자주 보게 되니 되도록이면 전체 경로를 사용해야 할 것이다.

참고 자료

5.13 - MyBatis Configuration XML File

MyBatis XML 설정 파일은 properties, settings, typeAliases, mappers 등 다양한 설정 항목으로 구성되며, 데이터베이스와의 상호작용을 정의하는 중요한 설정들을 포함한다. 이 파일은 MyBatis의 동작 방식과 데이터베이스 연결 환경을 관리하는 역할을 한다.

MyBatis Configuration XML File

MyBatis XML 설정파일은 다양한 셋팅과 프로퍼티를 가진다 해당 파일의 작성과 상세한 옵션 설정에 대해 알아본다.

Configuration XML

MyBatis XML 설정파일의 일반적인 구조 및 구성은 properties, settings, typeAliases, typeHandlers, objectFactory, plugins, environments, databaseIdProvider, mappers 등의 내용으로 구성이 되어 있으며 주요 내용은 아래와 같다.

Sample Configuration

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
 
<configuration>
 
	<properties resource="org/mybatis/example/config.properties">
		<property name="username" value="dev_user"/>
		<property name="password" value="F2Fa3!33TYyg"/>
	</properties>
 
	<settings>
		<setting name="cacheEnabled" value="true"/>
		<setting name="lazyLoadingEnabled" value="true"/>
		<setting name="multipleResultSetsEnabled" value="true"/>
	</settings>
 
	<typeHandlers>
		<typeHandler handler="egovframework.rte.psl.dataaccess.typehandler.CalendarMapperTypeHandler" />
	</typeHandlers>
 
	<typeAliases>
		<typeAlias alias="deptVO" type="egovframework.rte.psl.dataaccess.vo.DeptVO" />
		<typeAlias alias="empVO" type="egovframework.rte.psl.dataaccess.vo.EmpVO" />
	.
	.
	. 
 
</typeAliases>
 
.
.
.
.
 
</configuration>
  • properties : 표준 java properties (key=value 형태)파일에 대한 연결을 지원하며 설정 파일내에서 ${key} 와 같은 형태로 properties 형태로 외부화해놓은 실제 값(여기서는 DB 접속 관련 driver, url, id/pw)을 참조할 수 있다. resource 속성으로 classpath, url 속성으로 유효한 URL 상에 있는 자원을 지정 가능하다.

  • settings : 런타임시 MyBatis의 행위를 조정하기 위한 옵션 설정을 통해 최적화할 수 있도록 지원한다. 다음표는 셋팅과 그 의미 그리고 디폴트 값을 설명한다.

셋팅설명사용가능한 값들디폴트
cacheEnabled설정에서 각 mapper 에 설정된 캐시를 전역적으로 사용할지 말지에 대한 여부true / falseTRUE
lazyLoadingEnabled늦은 로딩을 사용할지에 대한 여부. 사용하지 않는다면 모두 즉시 로딩할 것이다.true / falseTRUE
aggressiveLazyLoading활성화 상태로 두게 되면 늦은(lazy) 로딩 프로퍼티를 가진 객체는 호출에 따라 로드될 것이다. 반면에 개별 프로퍼티는 요청할때 로드된다.true / falseTRUE
multipleResultSetsEnabled한개의 구문에서 여러개의 ResultSet 을 허용할지의 여부(드라이버가 해당 기능을 지원해야 함)true / falseTRUE
useColumnLabel칼럼명 대신에 칼럼라벨을 사용. 드라이버마다 조금 다르게 작동한다. 문서와 간단한 테스트를 통해 실제 기대하는 것처럼 작동하는지 확인해야 한다.true / falseTRUE
useGeneratedKeys생성키에 대한 JDBC 지원을 허용. 지원하는 드라이버가 필요하다. true 로 설정하면 생성키를 강제로 생성한다. 일부 드라이버(예를들면, Derby)에서는 이 설정을 무시한다.true / falseFALSE
autoMappingBehaviorMyBatis 가 칼럼을 필드/프로퍼티에 자동으로 매핑할지와 방법에 대해 명시. PARTIAL 은 간단한 자동매핑만 할뿐, 내포된 결과에 대해서는 처리하지 않는다. FULL 은 처리가능한 모든 자동매핑을 처리한다.NONE, PARTIAL, FULLPARTIAL
defaultExecutorType디폴트 실행자(executor) 설정. SIMPLE 실행자는 특별히 하는 것이 없다. REUSE 실행자는 PreparedStatement 를 재사용한다. BATCH 실행자는 구문을 재사용하고 수정을 배치처리한다.SIMPLE REUSE BATCHSIMPLE
defaultStatementTimeout데이터베이스로의 응답을 얼마나 오래 기다릴지를 판단하는 타임아웃을 셋팅양수셋팅되지 않음(null)
safeRowBoundsEnabled중첩구문내 RowBound 사용을 허용true / falseFALSE
mapUnderscoreToCamelCase전통적인 데이터베이스 칼럼명 형태인 A_COLUMN을 CamelCase형태의 자바 프로퍼티명 형태인 aColumn으로 자동으로 매핑하도록 함true / falseFALSE
localCacheScopeMyBatis uses local cache to prevent circular references and speed up repeated nested queries. By default (SESSION) all queries executed during a session are cached. If localCacheScope=STATEMENT local session will be used just for statement execution, no data will be shared between two different calls to the same SqlSession.SESSION / STATEMENTSESSION
jdbcTypeForNullSpecifies the JDBC type for null values when no specific JDBC type was provided for the parameter. Some drivers require specifying the column JDBC type but others work with generic values like NULL, VARCHAR or OTHER.JdbcType enumeration. Most common are: NULL, VARCHAR and OTHEROTHER
lazyLoadTriggerMethodsSpecifies which Object’s methods trigger a lazy loadA method name list separated by commasequals,clone,hashCode,toString
defaultScriptingLanguageSpecifies the language used by default for dynamic SQL generation.A type alias or fully qualified class name.org.apache.ibatis.scripting.xmltags.XMLDynamicLanguageDriver
callSettersOnNullsSpecifies if setters or map’s put method will be called when a retrieved value is null. It is useful when you rely on Map.keySet() or null value initialization. Note primitives such as (int,boolean,etc.) will not be set to null.true / falseFALSE
logPrefixSpecifies the prefix string that MyBatis will add to the logger names.Any StringNot set
logImplSpecifies which logging implementation MyBatis should use. If this setting is not present logging implementation will be autodiscovered.SLF4J / LOG4J / LOG4J2 / JDK_LOGGING / COMMONS_LOGGING / STDOUT_LOGGING / NO_LOGGINGNot set
proxyFactorySpecifies the proxy tool that MyBatis will use for creating lazy loading capable objects.CGLIB / JAVASSISTCGLIB
  • typeAliases : 타입 별칭을 통해 자바 타입에 대한 좀더 짧은 이름을 사용할 수 있다. 오직 XML 설정에서만 사용되며, 타이핑을 줄이기 위해 사용된다.

  • typeHandler : MyBatis 가 PreparedStatement 에 파라미터를 셋팅하고 ResultSet 에서 값을 가져올때마다, TypeHandler 는 적절한 자바 타입의 값을 가져오기 위해 사용된다. typeHandler 구현체를 등록하여 사용할 수 있다.

  • objectFactory : MyBatis 는 결과 객체의 인스턴스를 만들기 위해 ObjectFactory를 사용한다.

  • mappers: 매핑된 SQL 구문을 정의한다. 해당 매퍼 파일의 지정은 클래스패스에 상대적으로 리소스를 지정할 수도 있고, url 을 통해서 지정할 수 도 있다

이외에도 plugins (매핑 구문을 실행하는 어떤 시점에 호출을 가로챈다. 기본적으로 MyBatis 는 메서드 호출을 가로채기 위한 플러그인을 허용한다), environments (여러개의 환경으로 설정), databaseIdProvider(데이터베이스 제품마다 다른 구문을 실행) 등의 설정을 통해 다양한 환경 및 시점에서 추가적인 설정이 가능하다.

5.14 - Mapper XML Files

MyBatis Mapper XML 파일은 SQL문을 정의하고, Parameter Object를 받아 SQL문을 실행하며, 그 결과를 Result Object에 자동으로 바인딩하는 기능을 제공한다. 이를 통해 SQL 실행과 데이터 매핑을 쉽게 처리할 수 있다.

Mapper XML File

MyBatis Mapper XML (SQL Mapping XML) File은 실행할 SQL문을 정의해놓은 파일로서,
SQL문 실행을 위해 Parameter Object를 받아오거나 SQL문 실행 결과를 Result Object에 자동 바인딩하는 기능 등을 제공한다.

Mapper XML (SQL Mapping XML)

Mapper XML File에는 다음과 같은 요소들을 사용할 수 있다.

  • <select>: 매핑된 SELECT 구문
  • <insert>: 매핑된 INSERT 구문
  • <update>: 매핑된 UPDATE 구문
  • <delete>: 매핑된 DELETE 구문
  • <sql>: 다른 구문에서 재사용하기 위한 SQL 조각
  • <resultMap>: 데이터베이스 결과 데이터를 객체에 매핑하는 방법을 정의
  • <cache>: 자신의 namespace를 위한 캐시설정
  • <cache-ref>: 다른 namespace의 캐시설정을 참조

Sample Configuration

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="egovframework.rte.psl.dataaccess.DeptMapper"> <!-- MyBatis에서는 namespace를 필수로 설정해야함 -->

    <resultMap id="deptResult" type="egovframework.rte.psl.dataaccess.vo.DeptVO"> <!-- [주의] iBatis의 class속성 -> type속성으로 변경 -->
        <result property="deptNo" column="DEPT_NO" />
        <result property="deptName" column="DEPT_NAME" />
        <result property="loc" column="LOC" />
    </resultMap>

    <select id="selectDept" parameterType="int" resultMap="deptResult"> <!-- [주의] iBatis의 parameterClass속성 -> parameterType속성으로 변경 -->
        <![CDATA[
            select DEPT_NO, DEPT_NAME, LOC
            from DEPT
            where DEPT_NO = #{deptNo}
        ]]>
    </select>

    <insert id="insertDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO">
        <![CDATA[
            insert into DEPT
                       (DEPT_NO, DEPT_NAME, LOC)
            values     (#{deptNo}, #{deptName}, #{loc})
        ]]>
    </insert>

    <update id="updateDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO">
        <![CDATA[
            update DEPT
            set    DEPT_NAME = #{deptName},
                   LOC = #{loc}
            where  DEPT_NO = #{deptNo}
        ]]>
    </update>

    <delete id="deleteDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO">
        <![CDATA[
            delete from DEPT
            where       DEPT_NO = #{deptNo}
        ]]>
    </delete>

</mapper>

<select>

<select>는 SELECT문을 작성할 때 사용되는 요소로, 데이터베이스에서 조회한 결과를 오브젝트에 매핑하는 역할을 한다.

먼저 <select> 요소에서 사용 가능한 속성들에 대해 알아보고, 위 샘플코드에서 언급된 <select> 설정을 자세히 살펴보자.

  • 속성
속성설명
id해당 구문을 호출할 때 사용되는 값으로, 각 SQL문을 구분하는 유일한 식별자 (필수)
parameterType해당 구문에 전달될 파라미터의 패키지 경로를 포함한 전체 클래스명이나 별칭
parameterMap<select> 외부에 정의된 Parameter Object(<parameterMap>)를 참조하는 방법, 권장하지 않음. parameterType 속성이나 Inline Parameter를 권장
resultType해당 구문이 리턴하는 타입의 패키지 경로를 포함한 전체 클래스명이나 별칭
resultMap<select> 외부에 정의된 Result Object(<resultMap>)를 참조하는 방법
flushCache이 속성값이 true이면, 구문이 호출될 때마다 캐시가 지원됨 (default: false)
useCache이 속성값이 true이면, 구문의 결과가 캐시됨 (default: true)
timeout예외가 던져지기 전에 데이터베이스의 요청 결과를 기다리는 최대 시간 (드라이버에 따라 다소 지원되지 않을 수 있음)
  • 기본 예제
<select id="selectDept" parameterType="int" resultMap="deptResult">
    <![CDATA[
        select DEPT_NO,
               DEPT_NAME,
               LOC
        from DEPT
        where DEPT_NO = #{deptNo} <!-- 파라미터 바인딩 표기법 #{property} -->
    ]]>
</select>

위의 <select> 구문은 ‘selectDept’를 이용하여 호출하며, ‘int’ 타입의 파라미터를 받아와 WHERE절 조건식에 바인딩하고,
SELECT 결과를 ‘deptResult’라는 이름을 가진 <resultMap> 설정에 따라 오브젝트에 매핑하여 리턴한다.

<resultMap id="deptResult" ... /> 형태로 <select> 외부에 정의되어 있다.

<insert>, <update>, <delete>

<insert>, <update>, <delete>는 각각 INSERT, UPDATE, DELETE문을 작성할 때 사용되는 요소로, 필요한 파라미터를 전달받아 데이터베이스의 데이터를 변경하는 역할을 한다.

먼저 <insert>, <update>, <delete> 요소에서 사용 가능한 속성들에 대해 알아보고, 위 샘플코드에서 언급된 설정을 각각 살펴보겠습니다.

1. <insert>

속성

속성설명
id해당 구문을 호출할 때 사용되는 값으로, 각 SQL문을 구분하는 유일한 식별자 (필수)
parameterType해당 구문에 전달될 파라미터의 패키지 경로를 포함한 전체 클래스명이나 별칭
flushCache이 속성값이 true이면, 구문이 호출될 때마다 캐시가 지원됨 (default: false)
timeout예외가 던져지기 전에 데이터베이스의 요청 결과를 기다리는 최대 시간 (드라이버에 따라 다소 지원되지 않을 수 있음)
statementTypeSTATEMENT / PREPARED / CALLABLE 중 선택, MyBatis에게 Statement, PreparedStatement 또는 CallableStatement를 사용하게 함 (default: PREPARED)
useGeneratedKeysDB 내부에서 생성한 키를 받는 JDBC getGeneratedKeys 메서드를 사용하도록 설정 (default: false)
keyPropertygetGeneratedKeys 메서드나 INSERT 구문의 selectKey 하위 요소에 의해 리턴된 키를 셋팅할 프로퍼티를 지정
keyColumn생성키를 가진 테이블의 칼럼명을 셋팅 (키 칼럼이 테이블의 첫번째 칼럼이 아닐 경우 필요)
  • 기본 예제
<insert id="insertDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO">
    <![CDATA[
        insert into DEPT (DEPT_NO, DEPT_NAME, LOC)
        values (#{deptNo}, #{deptName}, #{loc}) <!-- 파라미터 바인딩 표기법 #{property} -->
    ]]>
</insert>

위의 <insert> 구문은 ‘insertDept’를 이용하여 호출하며, ’egovframework.rte.psl.dataaccess.vo.DeptVO’ 타입의 파라미터를 받아와 INSERT 절에 바인딩한다.

2. <update>

속성

속성설명
id해당 구문을 호출할 때 사용되는 값으로, 각 SQL문을 구분하는 유일한 식별자 (필수)
parameterType해당 구문에 전달될 파라미터의 패키지 경로를 포함한 전체 클래스명이나 별칭
flushCache이 속성값이 true이면, 구문이 호출될 때마다 캐시가 지원됨 (default: false)
timeout예외가 던져지기 전에 데이터베이스의 요청 결과를 기다리는 최대 시간 (드라이버에 따라 다소 지원되지 않을 수 있음)
statementTypeSTATEMENT / PREPARED / CALLABLE 중 선택, MyBatis에게 Statement, PreparedStatement 또는 CallableStatement를 사용하게 함 (default: PREPARED)

기본 예제

<update id="updateDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO">
    <![CDATA[
        update DEPT
        set    DEPT_NAME = #{deptName}, <!-- 파라미터 바인딩 표기법 #{property} -->
               LOC = #{loc}
        where  DEPT_NO = #{deptNo}
    ]]>
</update>

위의 구문은 ‘updateDept’를 이용하여 호출하며, ’egovframework.rte.psl.dataaccess.vo.DeptVO’ 타입의 파라미터를 받아와 UPDATE절에 바인딩한다.

3. <delete>

  • 속성
속성설명
id해당 구문을 호출할 때 사용되는 값으로, 각 SQL문을 구분하는 유일한 식별자 (필수)
parameterType해당 구문에 전달될 파라미터의 패키지 경로를 포함한 전체 클래스명이나 별칭
flushCache이 속성값이 true이면, 구문이 호출될 때마다 캐시가 지원됨 (default: false)
timeout예외가 던져지기 전에 데이터베이스의 요청 결과를 기다리는 최대 시간 (드라이버에 따라 다소 지원되지 않을 수 있음)
statementTypeSTATEMENT / PREPARED / CALLABLE 중 선택, MyBatis에게 Statement, PreparedStatement 또는 CallableStatement를 사용하게 함 (default: PREPARED)
  • 기본 예제
<delete id="deleteDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO">
    <![CDATA[
        delete from DEPT
        where DEPT_NO = #{deptNo} -- 파라미터 바인딩 표기법 #{property}
    ]]>
</delete>

위의 <delete> 구문은 deleteDept를 이용하여 호출하며, egovframework.rte.psl.dataaccess.vo.DeptVO 타입의 파라미터를 받아와 WHERE절에 바인딩한다.

참고

parameterType에 지정한 전체 클래스명이 복잡하다면, 해당 클래스타입에 대한 Alias(별칭)로 대체할 수 있다.

<!-- In MyBatis Configuration XML File -->
<typeAlias type="egovframework.rte.psl.dataaccess.vo.DeptVO" alias="deptVO" />

<!-- Mapper XML File -->
<delete id="deleteDept" parameterType="deptVO">
    <![CDATA[
        delete from DEPT
        where DEPT_NO = #{deptNo} -- 파라미터 바인딩 표기법 #{property}
    ]]>
</delete>

<sql>

<sql> 요소는 다른 구문에서 재사용 가능한 SQL구문을 정의할 때 사용한다.

설정을 통해 재사용할 수 있다.

  • 기본예제
<sql id="userColumns"> id, username, password </sql>

<select id="selectUsers" resultType="map">
    <![CDATA[
        select <include refid="userColumns"/>
        from some_table
        where id = #{id}
    ]]>
</select>

Parameters

예제 코드에서 파라미터를 전달하는 간단한 구문을 살펴보도록 하겠다.

<insert id="insertDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO">
    <![CDATA[
        insert into DEPT
                   (DEPT_NO,
                    DEPT_NAME,
                    LOC)
        values     (#{deptNo},
                    #{deptName},
                    #{loc})
    ]]>
</insert>

위에서 egovframework.rte.psl.dataaccess.vo.DeptVO 클래스 타입의 객체가 mapper 오브젝트를 통해 전달될 경우 해당 객체의 deptNo, deptName, loc를 찾아서 PreparedStatement 파라미터로 전달된다.

추가적으로 파라미터 전달 시 파라미터에 다음과 같은 형태로 데이터 타입을 명시할 수 있다:

#{property, javaType=int, jdbcType=NUMERIC}

<resultMap>

<resultMap>는 SELECT 조회 결과값을 오브젝트에 매핑하기 위해 사용하는 요소로, ResultSet에서 데이터를 가져올 때 필요한 JDBC 코드를 대신한다.

  • 속성
속성설명
idStatement에서 resultMap을 참조하기 위한 유일한 식별자
type구문이 리턴한 결과를 매핑할 오브젝트 타입의 패키지 경로를 포함한 전체 클래스명이나 별칭
  • 기본예제
<!-- 기본 예제 코드 -->
<resultMap id="deptResult" type="egovframework.rte.psl.dataaccess.vo.DeptVO">
    <result property="deptNo" column="DEPT_NO" />
    <result property="deptName" column="DEPT_NAME" />
    <result property="loc" column="LOC" />
</resultMap>

<select id="selectDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO" resultMap="deptResult">
		<![CDATA[
			select DEPT_NO,DEPT_NAME,LOC
			from   DEPT
			where  DEPT_NO = #{deptNo}
		]]>
	</select>

SELECT문 결과값은 <select>에 지정한 resultMap 속성값에 따라 <resultMap id="deptResult">에 매핑되어 리턴된다.

또는 다음과 같이 <select>에서 resultMap 속성 대신 resultType 속성을 사용하여 결과값을 매핑할 클래스 타입을 지정할 수도 있다.

단, 아래와 같이 resultType 속성을 이용하는 경우에는 DB 컬럼명과 프로퍼티명이 동일해야 한다.

<select id="selectDept" parameterType="egovframework.rte.psl.dataaccess.vo.DeptVO" resultType="egovframework.rte.psl.dataaccess.vo.DeptVO">
    <![CDATA[
        select deptno, deptname, loc
        from   DEPT
        where  deptno = #{deptno}
    ]]>
</select>

위의 두 코드는 SELECT문의 칼럼명과 Result Object(DeptVO)의 프로퍼티명을 비교하여 일치하는 프로퍼티의 setter를 찾아 결과값을 매핑한다.

  • DB 컬럼명과 프로퍼티명이 다를 경우
<!-- 1) resultType 속성 사용 시 -->
<select id="selectDept" resultType="deptVO">
    <![CDATA[
        select
            DEPT_NO     as "deptNo",
            DEPT_NAME   as "deptName",
            LOC         as "loc"
        from DEPT
        where DEPT_NO = #{deptNo}
    ]]>
</select>
<!-- 2) <resultMap> 사용 시 -->
<resultMap id="deptResult" type="deptVO">
    <result property="deptNo" column="DEPT_NO" />
    <result property="deptName" column="DEPT_NAME" />
    <result property="loc" column="LOC" />
</resultMap>

<select id="selectDept" parameterType="deptVO" resultMap="deptResult">
    <![CDATA[
        select DEPT_NO, DEPT_NAME, LOC
        from   DEPT
        where  DEPT_NO = #{deptNo}
    ]]>
</select>

1)번의 경우, 컬럼 Alias를 이용하여 프로퍼티명과 일치시킨다.

2)번의 경우, 의 하위 요소인 을 이용하여 명시적으로 property명과 column명을 일치시킨다.

[참고] ResultSet 처리를 위해 hashmap 등으로 키 형태로 매핑하여 결과를 받아올 수 있지만, 좋지 않은 방법이므로 resultMap의 도메인 모델로는 자바빈이나 POJO를 사용한다.

<cache>

MyBatis는 쉽게 설정 가능하고 변경 가능한 쿼리 캐싱 기능을 가지고 있다. MyBatis 3 캐시 구현체는 좀 더 쉽게 설정할 수 있도록 많은 부분이 수정되었다.

성능을 개선하고 순환하는 의존성을 해결하기 위해 필요한 로컬 세션 캐싱을 제외하고 기본적으로 캐시가 작동하지 않는다. 캐싱을 활성화하기 위해서, SQL 매핑 파일에 다음 한 줄을 추가하면 된다.

  • <cache/>
  • 디폴트 설정내용
  1. 매핑 파일 내의 모든 <select> 구문의 결과는 캐시된다.
  2. 매핑 파일 내의 모든 <insert>, <update>, <delete> 구문이 호출될 때 캐시가 플러시된다.

이 외에 캐시 설정에 대해서는 MyBatis 매뉴얼을 참고하시길 바랍니다.

<cache-ref>

MyBatis 에서는 이전 섹션 내용을 돌이켜보면서, 특정 명명공간을 위한 캐시는 오직 하나만 사용되거나 같은 명명공간내에서는 구문마다 캐시를 지울수 있다.

명명공간간의 캐시 설정과 인스턴스를 공유하고자 할때가 있을 것이다.

이 경우 cache-ref 요소를 사용해서 다른 캐시를 참조할 수 있다.

  • <cache-ref namespace=”com.someone.application.data.SomeMapper” />

5.15 - Mapper Dynamic SQL

MyBatis는 다양한 조건에 따라 동적으로 SQL을 생성할 수 있는 강력한 동적 SQL 기능을 제공하며, OGNL 기반의 표현식을 통해 유연하고 편리하게 사용할 수 있다. 이는 iBatis보다 간단하고 직관적인 방식으로 동적 SQL 처리를 가능하게 한다.

Dynamic SQL

일반적으로 JDBC API를 사용한 코딩에서 다양한 조건에 따라 다양한 형태의 쿼리의 실행이 필요한 경우가 존재하며 이에 MyBatis는 강력한 동적 SQL 언어를 제공한다.

MyBatis는 SQL 문의 동적인 변경에 대해 iBatis보다 상대적으로 유연하다.
iBatis도 다양한 Dynamic 요소를 제공하였으나 이해해야 하는 요소들이 많았다.

MyBatis에서 제공하는 동적 SQL 요소들은 JSTL이나 XML 기반의 텍스트 프로세서와 유사한 형태로 제공되며 OGNL 기반의 표현식을 제공함으로써 보다 유연하고 편리하게 Dynamic 요소를 사용할 수 있다.

기본 Dynamic 요소 사용 방법

Sample Dynamic SQL mapper xml

MyBatis에서 제공하는 Dynamic 요소의 기본적인 형태에 대해 알아보도록 한다.
아래 Sql 매퍼 파일은 파라미터 객체의 empNo 속성의 값 유무에 따라 where EMP_NO = #{empNo} 조건절을 동적으로 추가/제거할 수 있는 예이다.

	<select id="selectJobHistListUsingDynamicElement" parameterType="egovframework.rte.psl.dataaccess.vo.JobHistVO" resultMap="egovframework.rte.psl.dataaccess.vo.JobHistVO">
		<![CDATA[
			select EMP_NO     as empNo,
			       START_DATE as startDate,
			       END_DATE   as endDate,
			       JOB        as job,
			       SAL        as sal,
			       COMM       as comm,
			       DEPT_NO    as deptNo
			from   JOBHIST
		]]>
		<where>
			<if test="empNo != null">
				EMP_NO = #{empNo}				
			</if>		
		</where>
	</select>

where 요소는 태그에 의해 콘텐츠가 리턴되면 단순히 “WHERE” 만을 추가한다. 하지만 하위 요소의 조건이 하나라도 만족하지 않으면 추가되지 않는다.
또한 콘텐츠가 “AND” 나 “OR”로 시작한다면, 그 “AND” 나 “OR”를 지워버리는 점에 유념하여 사용하도록 한다.

if

if는 가장 많이 사용되는 Dynamic 요소로 문자열을 선택적으로 검색하는 기능을 제공한다.
where 절 안에 사용되며 if 안에 해당 값이 존재하지 않으면 모든 결과값이 리턴된다.

        ..
	<select id="selectEmployerList" parameterType="egovframework.rte.psl.dataaccess.vo.EmpVO" resultType="egovframework.rte.psl.dataaccess.vo.EmpVO">
		<![CDATA[
			select
				EMP_NO as empNo,
				EMP_NAME as empName,
				JOB as job,
				MGR as mgr,
				HIRE_DATE as hireDate,
				SAL as sal,
				COMM as comm,
				DEPT_NO as deptNo
			from EMP
		]]>
		<where>
			<if test="empNo != null">
				EMP_NO = #{empNo}
			</if>
			<if test="empName != null">
				EMP_NAME LIKE '%' || #{empName} || '%'
			</if>
		</where>
	</select>

전달된 인자의 특정 property에 대해 if로 비교하는 경우가 가장 많이 사용된다.

choose (when, otherwise)

모든 조건을 적용하는 대신 한 가지 조건 만을 적용해야 할 필요가 있는 경우 MyBatis에서 제공하는 choose 요소를 사용하며 이는 자바의 switch 구문과 유사한 개념이다.

아래 예제를 보면 지금은 MGR 정보만으로 검색하고, EMP 정보가 있다면 그 값으로 검색된다.
두 가지 값을 모두 제공하지 않는다면 HIRE 상태인 Employee 정보가 리턴될 것이다.

	<select id="selectEmployeeList" parameterType="egovframework.rte.psl.dataaccess.vo.EmpVO" resultType="egovframework.rte.psl.dataaccess.vo.EmpVO">
		SELECT * FROM EMP WHERE JOB = 'Engineer'
		<choose>
			<when test="mgr ! null">
				AND MGR like #{mgr}
			</when>
			<when test="empNo ! null and empName ! =null">
				AND EMP_NAME like #{empName}
			</when>
			<otherwise>
				AND HIRE_STATUS = 'Y'
			</otherwise>
		</choose>
	</select>

trim (where, set)

아래 예제의 <trim prefix=“WHERE” prefixOverrides=“AND|OR”><where>와 동일하게 동작한다. 즉, where 요소에 대한 trim 기능을 제공한다.

	..
	<select id="selectEmployerList" parameterType="egovframework.rte.psl.dataaccess.vo.EmpVO"
		resultType="egovframework.rte.psl.dataaccess.vo.EmpVO">
		<![CDATA[
			select
				EMP_NO as empNo,
				EMP_NAME as empName,
				JOB as job,
				MGR as mgr,
				HIRE_DATE as hireDate,
				SAL as sal,
				COMM as comm,
				DEPT_NO as deptNo
			from EMP
		]]>
		<trim prefix="WHERE" prefixOverrides="AND|OR ">
			<if test="empNo != null">
				EMP_NO = #{empNo}
			</if>
			<if test="empName != null">
				EMP_NAME LIKE '%' || #{empName} || '%'
			</if>
		</trim>
	</select>

foreach

아래의 샘플 sql mapper xml 예를 참고하라. 일반적으로 iterate 태그 처리에 가장 많이 사용되는 in 조건절 처리 예이다.

foreach 요소는 매우 강력한 기능을 제공하는데 그중 하나가 collection을 명시하는 것을 허용하는 것이다.
foreach 요소에서는 item, index 두 가지 변수를 선언하며, 이 요소는 열고 닫는 문자열로 명시할 수 있고 반복 간에 둘 수 있는 구분자도 추가 가능하다.

	<select id="selectJobHistListUsingDynamicNestedIterate" parameterType="egovframework.rte.psl.dataaccess.util.EgovMap" resultMap="jobHistVO">
		<![CDATA[
			select EMP_NO     as empNo,
			       START_DATE as startDate,
			       END_DATE   as endDate,
			       JOB        as job,
			       SAL        as sal,
			       COMM       as comm,
			       DEPT_NO    as deptNo
			from   JOBHIST
		]]>
			where
			<foreach collection="condition" item="item" open="(" separator="and" close=")">
				${item.columnName} ${item.columnOperation} 
				<if test="item.nested == 'true'">
					<foreach item="item" index="index" collection="item.columnValue" open="(" separator="," close=")">
					      '${item}'
					</foreach>
				</if>
				<if test="item.nested != 'true'">
					#{item.columnValue}
				</if>
			</foreach>		
			order by EMP_NO, START_DATE
	</select>

5.16 - MyBatis 적용 가이드

전자정부 표준프레임워크 기반 MyBatis 적용 가이드이다.

MyBatis 적용 가이드

개요

전자정부 표준프레임워크 기반 MyBatis 적용 가이드이다.

📁 ex-dataaccess-mybatis.zip

Step 1. pom.xml 변경

표준프레임워크 dataaccess artifact version을 다음과 같이 2.7.0으로 변경한다.

<!-- 실행환경 라이브러리 -->
<dependency>
	<groupId>egovframework.rte</groupId>
	<artifactId>egovframework.rte.psl.dataaccess</artifactId>
	<version>2.7.0</version>
</dependency>

Step 2. 환경 설정

Step 2.1 XML 설정

Spring XML 설정 파일 상(ex: context-mapper.xml)에 다음과 같은 sqlSession bean을 추가한다.

Ex) context-mapper.xml

<!-- SqlSession setup for MyBatis Database Layer -->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionFactoryBean">
	<property name="dataSource" ref="dataSource" />
	<property name="mapperLocations" value="classpath:/sqlmap/mappers/**/*.xml" />
</bean>

Step 2.2 mapper xml 작성

MyBatis 가이드에 따라 query xml을 작성한다. (2.1의 예제 상 지정된 mapperLocations 위치)

Step 3. DAO 작성

DAO의 경우는 다음과 같이 3가지 방식이 가능하다.

방식설명비고
기존 DAO 클래스 방식@Repository 지정 및 EgovAbstractMapper extends 활용기존 iBatis와 같은 방식
Mapper interface 방식Mapper 인터페이스 작성 및 @Mapper annotation 지정@Mapper는 marker annotation(표준프레임워크 제공)
Annotation 방식query xml 없이 mapper 인터페이스 상 @Select, @Insert 등을 활용Dynamic SQL 등의 사용에 제약이 있음

3.1 기존 DAO 형태로 사용하는 경우

@Repository 지정된 class에 EgovAbstractMapper를 extends 하여 insert, update, delete, selectByPk, list 메서드를 활용한다.

@Repository("deptMapper")
public class DeptMapper extends EgovAbstractMapper {

    public void insertDept(String queryId, DeptVO vo) {
        insert(queryId, vo);
    }
 
    public int updateDept(String queryId, DeptVO vo) {
        return update(queryId, vo);
    }
 
    public int deleteDept(String queryId, DeptVO vo) {
        return delete(queryId, vo);
    }
 
    public DeptVO selectDept(String queryId, DeptVO vo) {
        return (DeptVO)selectByPk(queryId, vo);
    }
 
    @SuppressWarnings("unchecked")
    public List<DeptVO> selectDeptList(String queryId, DeptVO searchVO) {
        return list(queryId, searchVO);
    }
}

3.2 Mapper interface 사용 방식

Mapper 인터페이스 작성 시 다음과 같이 @Mapper annotation을 사용한다.

(패키지를 포함하는 클래스명 부분이 mapper xml 상의 namespace로 선택되고 인터페이스 메서드가 query id로 호출되는 방식)

@Mapper("employerMapper")
public interface EmployerMapper  {

    public List<EmpVO> selectEmployerList(EmpVO vo);
 
    public EmpVO selectEmployer(BigDecimal empNo);
 
    public void insertEmployer(EmpVO vo);
 
    public int updateEmployer(EmpVO vo);
 
    public int deleteEmployer(BigDecimal empNo);

}

이 경우는 xml 설정상에 다음과 같은 MapperConfigurer 설정이 필요하다.

Ex: context-mapper.xml

<bean class="egovframework.rte.psl.dataaccess.mapper.MapperConfigurer">
	<property name="basePackage" value="egovframework.rte.**.mapper" />
</bean>

basePackage에 지정된 패키지 안에서 @Mapper annotation을 스캔하는 설정이다.

⇒ @Mapper로 지정된 인터페이스를 @Service에서 injection 하여 사용한다.

public class EmployerMapperTest {

    @Resource(name = "employerMapper")
    EmployerMapper employerMapper;

    @Test
    public void testInsert() throws Exception {
        EmpVO vo = makeVO();

        // insert
        employerMapper.insertEmployer(vo);

        // select
        EmpVO resultVO = employerMapper.selectEmployer(vo.getEmpNo());

        // check
        checkResult(vo, resultVO);
    }
}

3.3 Annotation 사용 방식

mapper xml 작성 없이 Mapper 인터페이스 상에 @Select, @Insert, @Update, @Delete 등의 annotation을 통해 query가 지정되어 사용된다.

@Mapper("departmentMapper")
public interface DepartmentMapper  {

    @Select("select DEPT_NO as deptNo, DEPT_NAME as deptName, LOC as loc from DEPT where DEPT_NO = #{deptNo}")
    public DeptVO selectDepartment(BigDecimal deptNo);
    
    @Insert("insert into DEPT(DEPT_NO, DEPT_NAME, LOC) values (#{deptNo}, #{deptName}, #{loc})")
    public void insertDepartment(DeptVO vo);
    
    @Update("update DEPT set DEPT_NAME = #{deptName}, LOC = #{loc} WHERE DEPT_NO = #{deptNo}")
    public int updateDepartment(DeptVO vo);
    
    @Delete("delete from DEPT WHERE DEPT_NO = #{deptNo}")
    public int deleteDepartment(BigDecimal deptNo);

}

⇒ 이 경우는 별도의 mapper xml을 만들 필요는 없지만, dynamic query를 사용하지 못하는 등의 제약사항이 따른다.

5.17 - Spring Data

Spring Data는 관계형 및 비관계형 데이터베이스, map-reduce 프레임워크, 클라우드 기반 데이터 서비스 등 다양한 데이터 액세스 기술을 쉽게 사용할 수 있도록 지원하는 오픈 소스 프로젝트이다. 이를 통해 새로운 데이터 기술뿐만 아니라 관계형 데이터베이스에 대한 향상된 지원도 제공한다.

Spring Data

Spring Data는 데이터베이스 관련 많은 하위 프로젝트를 포함하는 오픈 소스 프로젝트로, non-relational databases, map-reduce frameworks, and cloud based data services 등의 새로운 데이터 액세스 기술을 보다 쉽게 사용 할 수 있는 기능을 제공한다. 또한 관계형 데이터베이스 기술에 대한 향상된 지원도 제공한다.

Spring Data Project

CategorySub-ProjectDescription
Relational DatabasesJPASpring Data JPA - Simplifies the development of creating a JPA-based data access layer
JDBC ExtensionsSupport for Oracle RAC, Advanced Queuing, and Advanced datatypes. Support for using QueryDSL with JdbcTemplate.
Big DataApache HadoopThe Apache Hadoop project is an open-source implementation of frameworks for reliable, scalable, distributed computing and data \storage.
Data-GridGemFireVMware vFabric GemFire is a distributed data management platform providing dynamic scalability, high performance, and database-like \persistence. It blends advanced techniques like replication, partitioning, data-aware routing, and continuous querying.
HTTPRESTSpring Data REST - Perform CRUD operations of your persistence model using HTTP and Spring Data Repositories.
Key Value StoresRedisRedis is an open source, advanced key-value store.
Document StoresMongoDBMongoDB is a scalable, high-performance, open source, document-oriented database.
Graph DatabasesNeo4jNeo4j is a graph database, a fully transactional database that stores data structured as graphs.
Column StoresHBaseApache HBase is an open-source, distributed, versioned, column-oriented store modeled after Google’ Bigtable. HBase functionality is part of the Spring for Apache Hadoop project.
Common InfrastructureCommonsProvides shared infrastructure for use across various data access projects. General support for cross-database persistence is located here

참고자료

5.18 - Spring Data Repository

Spring Data는 Repository를 추상화하여 Data Access Layer 구현을 최소화하고, 메소드명만으로 쿼리를 자동 생성하는 Query Method를 지원해 개발 생산성을 높인다. CrudRepository는 기본적인 CRUD 메소드를, PagingAndSortingRepository는 페이징 및 정렬 기능을 제공한다.

Repository

설명

일반적으로 Data Access Layer를 구현하기 위해서는 상당량의 코드를 작성해야 한다. Spring Data에서는 Repository를 추상화하여 다양한 저장소에 접근하기 위한 Data Access Layer 구현 코드를 최소화함으로써 개발생산성을 높일 수 있도록 한다. 이는 Query Method를 통해 가능한데 Query Method란 메소드명만 가지고 쿼리를 만들 수 있다는 것이다. 특정 규칙에 맞게 메소드를 작성하면 개발자가 따로 Data Access 클래스를 만들지 않아도 Spring Data가 대신하여 해당 Database와 자동으로 매핑하여 결과를 가져다준다.

Repository 인터페이스를 상속받아 CRUD 관련 메소드들을 제공하는 CrudRepository 인터페이스와 Paging 처리 기능이 제공되는 PagingAndSortingRepository 인터페이스를 살펴보도록 하겠다.

User Guide

CRUD 기능 제공 인터페이스

Spring Data 리파지토리 추상화의 핵심 인터페이스는 바로 Repository이다. Repository는 일종의 마커 인터페이스로 클래스 타입과 ID 타입을 이용해서 작성할 수 있으며 Repository 인터페이스의 하위 인터페이스로 CRUD 기능을 제공하는 CrudRepository가 있다. 다음은 CrudRepository 코드이다.

public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {

    <S extends T> S save(S entity);........................

    T findOne(ID primaryKey);..............................

    Iterable<T> findAll();.................................

    Long count();..........................................

    void delete(T entity);.................................

    boolean exists(ID primaryKey);.........................

}

❶ 전달받은 엔터티를 저장한다.

❷ 전달된 ID로 식별한 엔터티를 리턴한다.

❸ 모든 엔터티를 리턴한다.

❹ 엔터티의 갯수를 리턴한다.

❺ 전달받은 엔터티를 삭제한다.

❻ 전달된 ID에 해당하는 엔터티의 존재여부를 리턴한다.

CRUD 기능 제공 인터페이스

Spring Data에서는 CrudRepository 이외에도 페이징 처리 기능을 제공하는 PagingAndSortingRespository라는 인터페이스를 제공하며 이 인터페이스는 CrudRepository를 상속받고 있다.

public interface PagingAndSortingRepository<T, ID extends Serializable> extends CrudRepository<T, ID> {
    Iterable<T> findAll(Sort sort);
    Page<T> findAll(Pageable pageable);
}

PagingAndSortingRepository 인터페이스를 사용하여 2번째 페이지에서 20건의 데이터를 가져오는 예제이다.

PagingAndSortingRepository<User, Long> repository = 
Page<User> users = repository.findAll(new PageRequest(1, 20));

참고자료

5.19 - Query Method

JPA 모듈은 문자열로 쿼리를 정의하거나 메서드에서 파생되어진 쿼리를 사용하는 방법을 지원한다.

Query Method

설명

User Guide

Query Method

JPA 모듈은 문자열로 쿼리를 정의하거나 메서드에서 파생되어진 쿼리를 사용하는 방법을 지원한다.

쿼리생성

스트링으로 쿼리를 정의하는 예시 :

public interface UserRepository extends Repository<User, Long> {
    List<User> findByEmailAddressAndLastname(String emailAddress, String lastname);

JPA표준 API는 위의 스트링으로 정의된 쿼리를 다음 쿼리로 변경한다.

select u from User u where u.emailAddress = ?1 and u.lastname = ?2

지원하는 메서드 명

KeywordSampleJPQL snippet
AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2
OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2
BetweenfindByStartDateBetween… where x.startDate between 1? and ?2
LessThanfindByAgeLessThan… where x.age < ?1
GreaterThanfindByAgeGreaterThan… where x.age > ?1
AfterfindByStartDateAfter… where x.startDate > ?1
BeforefindByStartDateBefore… where x.startDate < ?1
IsNullfindByAgeIsNull… where x.age is null
IsNotNull,NotNullfindByAge(Is)NotNull… where x.age not null
LikefindByFirstnameLike… where x.firstname like ?1
NotLikefindByFirstnameNotLike… where x.firstname not like ?1
StartingWithfindByFirstnameStartingWith… where x.firstname like ?1 (parameter bound with appended %)
EndingWithfindByFirstnameEndingWith… where x.firstname like ?1 (parameter bound with prepended %)
ContainingfindByFirstnameContaining… where x.firstname like ?1 (parameter bound wrapped in %)
OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by x.lastname desc
NotfindByLastnameNot… where x.lastname <> ?1
InfindByAgeIn(Collection ages)… where x.age in ?1
NotInfindByAgeNotIn(Collection age)… where x.age not in ?1
TruefindByActiveTrue()… where x.active = true
FalsefindByActiveFalse()… where x.active = false

Using @Query

Using named queries to declare queries for entities is a valid approach and works fine for a small number of queries. As the queries themselves are tied to the Java method that executes them you actually can bind them directly using the Spring Data JPA @Query annotation rather than annotating them to the domain class. This will free the domain class from persistence specific information and co-locate the query to the repository interface.

쿼리메서드에 정의된 쿼리들은 xml에 선언된 @NamedQuery나 named queries보다 우선하여 처리됩니다.

@Query를 이용한 쿼리 선언 예제 :

public interface UserRepository extends JpaRepository<User, Long> {
    @Query("select u from User u where u.emailAddress = ?1")
    User findByEmailAddress(String emailAddress);
}

파라미터를 활용한 @Query

By default Spring Data JPA will use position based parameter binding as described in all the samples above. This makes query methods a little error prone to refactoring regarding the parameter position. To solve this issue you can use @Param annotation to give a method parameter a concrete name and bind the name in the query: 기본적으로 스프링 데이터 JPA는 위의 모든 샘플에 설명 된대로 파라미터가 바인딩 된 바인딩 위치 기반 매개 변수를 사용합니다.

파라미터를 이용한 쿼리 선언 예제 :

public interface UserRepository extends JpaRepository<User, Long> {
    @Query("select u from User u where u.firstname = :firstname or u.lastname = :lastname")
    User findByLastnameOrFirstname(@Param("lastname") String lastname, @Param("firstname") String firstname);
}

참고자료

5.20 -

5.21 - MongoDB support

Spring Data MongoDB는 MongoDB를 위한 Data Access 기능을 제공하는 하위 프로젝트로, MongoTemplate을 통한 효율적인 데이터 처리와 Annotation 기반 매핑, Java 기반의 Query 및 Criteria DSL을 지원한다. 또한, Spring의 데이터 액세스 예외 계층 구조 변환 기능과 Spring Data Repository 인터페이스 통합을 제공한다. 이를 통해 MongoDB와의 데이터 접근을 보다 쉽게 구현할 수 있다.

MongoDB support 3.5.1

설명

Spring Data MongoDB는 Spring Data의 하위 프로젝트로서 document-oriented storage를 지원하는 MongoDB에 대한 Data Access 기능을 제공한다.

MongoDB support 주요 기능

  • Spring configuration 지원 (@Configuration, XML namespace)
  • 기본 처리를 효율적으로 지원하는 MongoTemplate helper 제공
  • Spring이 제공하는 Data Access Exception hierarchy 변환 기능 제공
  • Spring의 Conversion Service와 통합된 Feature Rich Object Mapping 기능
  • Annotation 기반 매핑 metadata
  • Java 기반 Query, Criteria, Update DSLs
  • Spring Data의 Repository 인페이스 지원
  • QueryDSL 등

1. 시작하기

Spring Data MongoDB를 사용하기 위해서는 다음과 같은 dependency 추가가 필요하다.

<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-mongodb</artifactId>
    <version>1.7.2.RELEASE</version>
</dependency>

2. Spring 관련 설정

Mongo mongo-client, mongoTemplate 생성

Spring 기반에서는 다음과 같이 Mongo에 대한 인스턴스 생성이 필요하다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mongo="http://www.springframework.org/schema/data/mongo"
       xsi:schemaLocation=
               "http://www.springframework.org/schema/context
          http://www.springframework.org/schema/context/spring-context-4.0.xsd
          http://www.springframework.org/schema/data/mongo
          http://www.springframework.org/schema/data/mongo/spring-mongo.xsd
          http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">
    <!-- Default bean name is 'mongo' -->
    <mongo:mongo-client
            host="localhost"
            port="27017"
            credentials="id:password@database"
            id="mongo">
    </mongo:mongo-client>

</beans>

추가적으로 MongoDB가 replica 방식으로 굿어된 경우는 다음과 같이 replica-set 설정을 지정하면 된다.

<mongo:mongo id="replicaSetMongo" replica-set="127.0.0.1:27017,localhost:27018"/>

MongoDBFactory 생성

다음으로 Mongo 인스턴스와 연결을 위한 MongoDBFactory이 필요한데, XML 기반의 설정에서는 다음과 같이 지정할 수 있다.

<!-- Default bean name is 'mongoDbFactory' -->
<mongo:db-factory dbname="database" mongo-ref="mongo" id="mongoDbFactory" />

MongoTemplate 생성

실제 mongoDB에 대한 처리(operations)를 위하여 MongoTemplate을 생성한다.

MontoTemplate의 다은과 같은 생성자를 통해 생성될 수 있다.

  • MongoTemplate(Mongo mongo, String databaseName)
  • MongoTemplate(Mongo mongo, String databaseName, UserCredentials userCredentials) : 사용자 접속 정보 추가
  • MongoTemplate(MongoDbFactory mongoDbFactory) : MogoDbFactory(com.mongodb.Mongo object, database name 및 접속 계정 포함)를 통한 연결
  • MongoTemplate(MongoDbFactory mongoDbFactory, MongoConverter mongoConverter) : 매핑을 위한 MongoConverter 사용

XML 설정일 경우는 다음과 같이 생성할 수 있다.

<mongo:mongo-client
        host="localhost"
        port="27017"
        credentials="id:password@database"
        id="mongo">
</mongo:mongo-client>

<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
    <constructor-arg ref="mongo"/>
    <constructor-arg name="databaseName" value="geospatial"/>
</bean>

MongoDbFactory를 사용하는 경우 다음과 같이 지정할 수 있다.

<!-- Default bean name is 'mongo' -->
<mongo:mongo-client
        host="localhost"
        port="27017"
        credentials="id:password@xxx"
        id="mongo">
</mongo:mongo-client>

<!-- for Replica Sets -->
<!-- mongo:mongo id="replicaSetMongo" replica-set="127.0.0.1:27017,127.0.0.1:27018" /-->

<!-- Default bean name is 'mongoDbFactory' -->
<mongo:db-factory dbname="database" mongo-ref="mongo" id="mongoDbFactory" />

<bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
    <constructor-arg name="mongoDbFactory" ref="mongoDbFactory" />
</bean>

기본 처리

MongoTemplate는 저장, 수정, 삭제 및 object 매팽 등의 기본 기능을 제공한다.

다음은 Person 객체에 대한 간단한 기본 예제이다.

Person 클래스

public class Person {

    private String id;
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getId() {
        return id;
    }
    public String getName() {
        return name;
    }
    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person [id=" + id + ", name=" + name + ", age=" + age + "]";
    }

}

Test 클래스

@Resource(name="mongoTemplate")
private  MongoTemplate mongoTemplate;
 
@Test
public void testBasicOperations() {
 
	Person person = new Person("Joe", 34);
 
	// Insert
	mongoTemplate.insert(person);
	LOGGER.info("Insert : " + person);
 
	// Find
	person = mongoTemplate.findOne(new Query(where("name").is("Joe")), Person.class);
	LOGGER.info("Found: " + person);
 
	assertEquals("Joe", person.getName());
 
	// Update
	mongoTemplate.updateFirst(query(where("name").is("Joe")), update("age", 35), Person.class);    
	person = mongoTemplate.findOne(query(where("name").is("Joe")), Person.class);
	LOGGER.info("Updated: " + person);
 
	assertEquals(35, person.getAge());
 
	// Delete
	mongoTemplate.remove(person);
 
	// Check that deletion worked
	List<Person> people =  mongoTemplate.findAll(Person.class);
	LOGGER.info("Number of people = : " + people.size());
	assertEquals(0, people.size());
 
	mongoTemplate.dropCollection("person");
 
}

※ 내부적으로 MongoConverter에 의해 String과 ObjectId에 대해서는 자동 변환 처리됨

_id 필드 처리

MongoDB는 모든 문서에 대하여 ‘_id’ 필드를 가져야 한다. 그렇지 않은 경우 내부적으로 자동생성되는 ObjectId로 처리된다. 그러나 MongoMappingConverter가 사용되면 다음과 같은 규칙에 의해 ‘_id’ 필드에 대한 매핑을 처리한다.

  • @Id(org.springframework.data.annotation.Id)가 지정된 property가 ‘_id’에 매핑된다.
  • id라는 이름의 property는 ‘_id’로 매핑된다. (위 Person 예제의 경우 이에 해당)

문서 저장 처리(save, insert)

MongoTemplate에는 객체를 저장하기 위한 몇 개의 메소드를 제공한다. 가장 간단한 경우가 POJO를 저장하는 것이다. 이 경우 collection 이름은 클래스명(not fully qualifed)이 사용되면, 저장 메소스 호출 시 지정될 수도 있다.

다음은 저장 처리 메소드에 대한 정리이다.

  • void save(Object objectToSave) : 기본 collection에 저장
  • void save(Object objectToSave, String collectionName) : 지정된 collection에 저장
  • void insert(Object objectToSave) : 기본 collection에 등록
  • void insert(Object objectToSave, String collectionName) : 지정된 collection에 등록

※ MongoTemplate은 저장을 위해 save와 insert를 제공하는데, save의 경우는 기존 등록된 문서가 없는 경우 insert로 처리되며 존재하는 경우 덮어쓰기를 한다.

insert의 경우 기존 id가 존재하면 오류를 발생한다.

문서 수정 처리(update)

문서 수정 처리를 위해서는 첫번째 문서만을 수정하는 updateFirst 메소드와 모든 문서를 수정하는 updateMulti로 구성된다.

다음은 계정이 저축계좌(SAVINGS)인 모든 계좌에 50원을 추가하는 예제이다.

import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Query;
import static org.springframework.data.mongodb.core.query.Update;
 
    ...

    WriteResult wr = mongoTemplate.updateMulti(
            new Query(where("accounts.accountType").is(Account.Type.SAVINGS)),
            new Update().inc("accounts.$.balance", 50.00),
            Account.class);

Update class 저장 메소드

  • Update addToSet (String key, Object value) Update using the $addToSet update modifier
  • Update inc (String key, Number inc) Update using the $inc update modifier
  • Update pop (String key, Update.Position pos) Update using the $pop update modifier
  • Update pull (String key, Object value) Update using the $pull update modifier
  • Update pullAll (String key, Object[] values) Update using the $pullAll update modifier
  • Update push (String key, Object value) Update using the $push update modifier
  • Update pushAll (String key, Object[] values) Update using the $pushAll update modifier
  • Update rename (String oldName, String newName) Update using the $rename update modifier
  • Update set (String key, Object value) Update using the $set update modifier
  • Update unset (String key) Update using the $unset update modifier

참고자료

MongoDB에 대한 기본 처리는 MongoDB Manual을 참조하고, Spring과의 연동 부분에 대한 세부적인 처리는 Spring Data MongoDB에 대한 Reference를 참조한다.

5.22 - MongoDB Repositories

Spring Data MongoDB는 Spring Data의 repository 추상화 인터페이스를 지원하며, 자세한 내용은 Spring Data JPA 가이드의 Repository 섹션을 참조 한다.

MongoDB Repositories 3.5.1

설명

Spring Data MongoDB도 Spring Data repository 추상화 인터페이스를 지원한다. 이에 대한 내용은 Spring Data JPA 가이드 중 Repository 부분을 참조한다.

1. 시작하기

MongoDB에 대한 repository를 사용하기 위해서는 다음과 같은 mongo schem의 repositories 설정이 필요하다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mongo="http://www.springframework.org/schema/data/mongo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
		http://www.springframework.org/schema/data/mongo http://www.springframework.org/schema/data/mongo/spring-mongo.xsd">

    <mongo:mongo-client
            host="localhost"
            port="27017"
            credentials="id:password@database"  >
    </mongo:mongo-client>

    <mongo:db-factory dbname="database" mongo-ref="mongo" />

    <bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate">
        <constructor-arg name="mongoDbFactory" ref="mongoDbFactory" />
    </bean>

    <!-- for Repository -->
    <mongo:repositories base-package="egovframework.rte.psl.data.mongodb.repository" />

</beans>

Domain Object 생성

일반적인 POJO를 통해 MongoDB에 문서를 저장한다.

public class Person {
    @Id
    private String id;

    private String firstname;

    private String lastname;

    private Address address;

    private double[] location;
 
    ...

    // getters/setters

기본 repository interface

public interface PersonRepository extends MongoRepository<Person, Long> {

    // additional custom finder methods go here
}

Paging 처리

MongoRepository interface는 기본적으로 PagingAndSortingRepository를 extends하기 때문에 다음과 같은 기본 페이징 처리를 지원한다.

@Autowired
private PersonRepository repository;
 
...

@Test
public void readsFirstPageCorrectly() {

    Page<Person> persons = repository.findAll(new PageRequest(0, 10));

    LOGGER.info("Persons Total elements : " + persons.getTotalElements());

    assertTrue(persons.isFirst());
}

2. Query methods

Repository는 Spring Data에서 제공되는 keyword 기반의 query method를 사용할 수 있다.

ex:

public interface PersonRepository extends MongoRepository<Person, String> {

    List<Person> findByLastname(String lastname);

    Page<Person> findByFirstname(String firstname, Pageable pageable);

    Person findByShippingAddresses(Address address);

}

지원되는 keyword는 다음과 같다.

KeywordSampleLogic result
GreaterThanfindByAgeGreaterThan(int age){“age” : {“$gt” : age}}
GreaterThanEqualfindByAgeGreaterThanEqual(int age){“age” : {“$gte” : age}}
LessThanfindByAgeLessThan(int age){“age” : {“$lt” : age}}
LessThanEqualfindByAgeLessThanEqual(int age){“age” : {“$lte” : age}}
BetweenfindByAgeBetween(int from, int to){“age” : {“$gt” : from, “$lt” : to}}
InfindByAgeIn(Collection ages){“age” : {“$in” : [ages…]}}
NotInfindByAgeNotIn(Collection ages)“age” : {“$nin” : [ages…]}}
IsNotNull, NotNullfindByFirstnameNotNull(){“age” : {“$ne” : null}}
IsNull, NullfindByFirstnameNull(){“age” : null}
LikefindByFirstnameLike(String name){“age” : age} ( age as regex)
RegexfindByFirstnameRegex(String firstname){“firstname” : {“$regex” : firstname }}
(No keyword)findByFirstname(String name){“age” : name}
NotfindByFirstnameNot(String name){“age” : {“$ne” : name}}
NearfindByLocationNear(Point point){“location” : {“$near” : [x,y]}}
WithinfindByLocationWithin(Circle circle){“location” : {“$within” : {“$center” : [ [x, y], distance]}}}
WithinfindByLocationWithin(Box box){“location” : {“$within” : {“$box” : [ [x1, y1], x2, y2]}}}
IsTrue, TruefindByActiveIsTrue(){“active” : true}
IsFalse, FalsefindByActiveIsFalse(){“active” : false}
ExistsfindByLocationExists(boolean exists){“location” : {“$exists” : exists }}

3. Repository delete queries

위의 keyword를 delete..By 또는 remove..By 형태로 사용하는 경우 삭제 처리가 가능하다.

public interface PersonRepository extends MongoRepository<Person, String> {
    List <Person> deleteByLastname(String lastname);

    Long deletePersonByLastname(String lastname);
}

4. Geo-spatial repository queries

Near keyword를 사용하는 경우 geo-spatial 처리와 관련된 query도 처리 가능하다.

public interface PersonRepository extends MongoRepository<Person, String> {
    // { 'location' : { '$near' : [point.x, point.y], '$maxDistance' : distance}}
    List<Person> findByLocationNear(Point location, Distance distance);
}

5. MongoDB JSON based query methods

org.springframework.data.mongodb.repository.Query anntation을 사용할 경우 JSON query 방식을 통해 직접 query를 지정할 수 있다.

public interface PersonRepository extends MongoRepository<Person, String> {

    @Query("{ 'firstname' : ?0 }")
    List<Person> findByThePersonsFirstname(String firstname);
 
}

5.23 - ORM 서비스

ORM 서비스는 객체 모델링과 관계형 데이터 모델링의 불일치를 해결하기 위해 JPA와 Hibernate를 사용하며, DBMS 변경 시에도 설정 정보만 수정하면 코드 변경 없이 동작이 가능하다. Lazy Loading과 Cache 활용으로 성능을 향상시키며, Entity Class에 최소한의 Annotation을 사용해 매핑을 정의한다. SQL 처리 방식에 익숙한 개발자는 추가 학습이 필요할 수 있다.

ORM 서비스

개요

객체 모델링(Object Oriented Modeling)과 관계형 데이터 모델링(Relational Data Modeling) 사이의 불일치를 해결해 주는 OR Mapping 서비스로 자바 표준인 JPA를 표준 서비스로 제시하고 구현체로는 JPA 구현체중에 가장 성능이 우수한 것으로 알려진 Hibernate를 이용하였다. 서비스의 특징을 살펴보면 다음과 같다.

  • 특정 DBMS에 영향을 받지 않으므로 DBMS가 변경되더라도 데이터 액세스 처리 코드에 대한 변경없이 설정 정보의 변경만으로도 동작 가능하다.
  • SQL을 작성하고 SQL 실행 결과로부터 전달하고자 하는 객체로 변경하는 코드를 작성하는 시간이 줄어든다. 하지만 필요시 SQL 작업도 가능하다.
  • 기본적으로 필요 시점에만 DBMS에 접근하는 Lazy Loading 전략 채택하고 Cache활용을 통해 DBMS에 대한 접근 횟수를 줄여나가 어플리케이션의 성능 향상을 도모한다.
  • 별도의 XML 파일로 매핑을 관리하지 않고 Entity Class에 최소한의 Annotation으로 정의하므로써 작업이 용이하다.
  • Entity Class가 일반 클래스로 정의됨으로써 상속이나 다양성, 캡슐화 같은 것들을 그대로 적용하면서 퍼시스턴스 오브젝트로 사용할 수 있다.
  • 자바 표준이므로 많은 벤더들에서 구현체를 지원하고 개발을 편리하게 할 수 있는 JPA툴(Dali)을 지원한다.
  • SQL을 이용하여 처리하는 방식에 익숙한 개발자가 사용하려면 학습이 필요하고 이에 따른 장벽이 존재한다.

설명

주요 개념

옆의 그림에서 보는것과 같이 DBMS 기반의 어플리케이션 수행을 하기 위해 필요한 주요 구성 요소는 Entity, Persistence.xml 이며, 각각은 다음과 같은 역할을 수행한다.

  • Entity: 어플리케이션 실행 여부와 상관없이 물리적으로 존재하는 데이터들을 다룬다. 일반적으로 DBMS 데이터를 이용하는 어플리케이션을 개발할 경우 어플리케이션의 비즈니스 레이어에서 특정 DBMS에 맞는 SQL을 통해 어플리케이션의 데이터를 처리하게 된다. 그러나 JPA 기반의 어플리케이션에서는 Entity를 중심으로 하여 어플리케이션의 데이터와 DBMS 연동이 가능해진다. annotation 기반으로 매핑관련 사항도 Entity 클래스에서 정의할 수 있어서 별도의 파일없이 테이블과의 관계를 표현할 수 있다.
  • Persistence.xml: 구현체에 대한 선언 및 대상 엔티티 클래스 지정 구현체별 프로퍼티 지정등을 할 수 있는 설정 파일로 JPA를 이용해서 어플리케이션을 구동할 경우 필수적으로 작성을 해야하는 파일이다.
  • JPA(Hibernate) : JPA 구현체로의 Hibernate의 요소는 Hibernate Core , Hibernate Annotations , Hibernate EntityManager 로 되어 있으며 JPA 구성에 필요한 Entity Manager등 구현 클래스를 포함하고 있다.
  • JPA Tool : JPA 지원툴이 Eclipse Web Tools Platform내에 서브프로젝트로 Dali JPA Tools 가 있다. 이 툴을 활용함으로써 DB에 생성된 테이블로부터 Entity 클래스 생성등 손이 많이 가는 작업을 자동으로 처리할 수 있다. 자세한 정보는 Dali Homepage를 참조한다.

시작하기

ORM 서비스에 대한 자세한 설명에 앞서 간단하게 ORM 서비스를 시작하는데 필요한 것에 대한 설명을 하고자 한다.

Step1. 사전 준비

필요 Library

본 서비스를 활용하기 위해서 필요한 Library 목록과 설명은 아래와 같다.

라이브러리설명연관 라이브러리
antlr-2.7.7.jar파서 라이브러리
commons-collections-3.2.jarcollection 처리를 위한 라이브러리
commons-dbcp-1.2.2.jarDataSource 커넥션 풀 라이브러리
commons-logging-1.1.1.jarLogging 처리를 위한 라이브러리hibernate-annotations-3.4.0.GA.jar에서 참조
log4j-1.3alpha-8.jarLogging 처리를 위한 라이브러리
slf4j-api-1.5.3.jarLogging 처리를 위한 라이브러리
slf4j-log4j12-1.5.3.jarLogging 처리를 위한 라이브러리
commons-pool-1.3.jarpooling 처리를 위한 라이브러리commons-dbcp-1.2.2.jar에서 참조
dom4j-1.6.1.jarXML 파싱 라이브러리hibernate-3.2.4.ga.jar 에서 참조
ejb3-persistence-1.0.2.GA.jarJPA Interface 클래스 라이브러리
hibernate-annotations-3.4.0.GA.jarHibernate Annotation
hibernate-entitymanager-3.4.0.GA.jarHibernate Entity Manager 구현체 라이브러리
hibernate-commons-annotations-3.1.0.GA.jarHibernate 공통 annotation 라이브러리hibernate-entitymanager-3.4.0.GA.jar에서 참조
hibernate-core-3.3.0.SP1.jarHiberante Core 라이브러리hibernate-entitymanager-3.4.0.GA.jar에서 참조
javassist-3.4.GA.jar자바 bytecode 조작 라이브러리hibernate-entitymanager-3.4.0.GA.jar에서 참조
jta-1.1.jarJTA 인터페이스 라이브러리hibernate-entitymanager-3.4.0.GA.jar에서 참조
hsqldb-1.8.0.10.jarHSQL JDBC 드라이버
mysql-connector-java-5.1.6.jarMYSQL JDBC 드라이버
ojdbc-14.jarORACLE JDBC 드라이버
junit-4.4.jar테스트 지원 라이브러리

Step2. Entity 클래스 생성

간단한 형태의 Entity 클래스를 생성한다. 네개의 Attribute로 구성되어 있고 각각의 getter,setter 메소드로 구성되어 있다.

Entity 클래스
@Entity
public class Department implements Serializable {
 
   private static final long serialVersionUID = 1L;
 
   @Id
   private String deptId;
 
   private String deptName;
 
   private Date createDate;
 
   private BigDecimal empCount;
 
   public String getDeptId() {
      return deptId;
   }
 
   public void setDeptId(String deptId) {
      this.deptId = deptId;
   }	   
   ...
}
  • @Entity : Department 가 Entity 클래스임을 정의
  • @Id : Primary Key 정보 지정

Step3. persistence.xml 생성

위에서 정의한 Entity 클래스를 가지고 JPA 수행하기 위한 프로퍼티 파일로 구현체제공 클래스정보,엔티티클래스정보,DB접속 정보,로깅정보,테이블자동생성정보를 포함하고 있다.

<persistence-unit name="PersistUnit" transaction-type="RESOURCE_LOCAL">
 
   <provider>org.hibernate.ejb.HibernatePersistence</provider>
 
   <class>egovframework.Department</class>
   <exclude-unlisted-classes/>
 
   <properties>
      <property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver"/>
      <property name="hibernate.connection.url" value="jdbc:hsqldb:mem:testdb"/>
      <property name="hibernate.connection.username" value="sa"/>
      <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/>
 
      <property name="hibernate.connection.autocommit" value="false"/>
      <property name="hibernate.show_sql" value="true"/>
      <property name="hibernate.format_sql" value="true"/>
      <property name="hibernate.hbm2ddl.auto" value="create"/>
   </properties>
 
</persistence-unit>
  • provider : 구현체 클래스 지정
  • class : 엔티티 클래스명 정의
  • exclude-unlisted-classes : 위에서 지정하지 않은 클래스는 엔티티라고 정의되어 있어도 제외
  • hibernate.connection.* : DB 연결정보로 각자 환경에 맞추어 변경 가능
  • hibernate.dialect : DBMS별 Hibernate 사용을 위한 클래스 ( 각 DBMS별로 별도로 정의됨 )
  • hibernate.connection.autocommit : 자동 커밋여부 설정 false로 했으므로 명시적인 commit 가 필요함
  • hibernate.show_sql : 로그에 SQL 포함
  • hibernate.format_sql : SQL을 포맷에 맞추어 출력
  • hibernate.hbm2ddl.auto : DDL 자동 생성 , Entity로 정의된 클래스에 대해서 자동으로 테이블 생성함

Step4. 테스트 클래스 생성

위에서 정의한 Department를 이용하여 입력,수정,조회,삭제 처리를 하는 것을 JUNIT형태로 구성하였다.

@Test
public void testDepartment() throws Exception {
 
   String modifyName = "Marketing Department";
   String deptId = "DEPT-0001";
   Department department = makeDepartment(deptId);
 
   // Entity Manager 생성
   emf = Persistence.createEntityManagerFactory("PersistUnit");
   em = emf.createEntityManager();	
 
   // 입력
   em.getTransaction().begin();
   em.persist(department);
   em.getTransaction().commit();	
 
   em.getTransaction().begin();
   Department departmentAfterInsert = em.find(Department.class, deptId );
   // 입력 확인
   assertEquals("Department Name Compare!",department.getDeptName(),departmentAfterInsert.getDeptName());
 
   // 수정
   departmentAfterInsert.setDeptName(modifyName);
   em.merge(departmentAfterInsert);
   em.getTransaction().commit();	
 
   em.getTransaction().begin();
   Department departmentAfterUpdate = em.find(Department.class, deptId );
   // 수정 확인
   assertEquals("Department Modify Name Compare!",modifyName,departmentAfterUpdate.getDeptName());
 
   // 삭제
   em.remove(departmentAfterUpdate);
   em.getTransaction().commit();	
 
   // 삭제 확인
   Department departmentAfterDelete = em.find(Department.class, deptId );
   assertNull("Department is Deleted!",departmentAfterDelete);
 
   em.close();
 
}
  • Persistence.createEntityManagerFactory : Entity Manager Factory 생성하기
  • emf.createEntityManager : Entity Manager 생성
  • em.getTransaction().begin() : Transaction 시작
  • em.getTransaction().commit(): Commit 처리
  • em.persist : Insert 처리
  • em.find : SELECT 처리
  • em.merge : UPDATE 처리
  • em.remove : DELETE 처리
  • assertEquals : 값이 같은지 비교하는 것 (JUnit 메소드)
  • assertNull : NULL 인지 확인하는 것 (JUnit 메소드)

Step5. 실행

  1. ormsimpleguide.zip 파일을 다운로드 받아서 압축을 푼다.
  2. 이클립스에서 압축 푼 폴더를 선택하여 프로젝트를 Import 한다.
  3. 프로젝트내 src 폴더에 Department.java, DepartmentTest.java, resources 폴더에 META-INF/persistence.xml, log4j.xml 가 정상적으로 있는지 확인한다.
  4. lib에 라이브러리 파일이 있는지 확인한다.
  5. DepartmentTest.java를 선택하여 마우스 오른쪽 클릭하여 Run As > JUnit Test 실행한다.
  6. JUnit 결과창에서 정상적으로 수행된 것을 확인한다. ※ ORACLE 이나 MySQL의 경우는 첨부되는 persistence.xml 의 주석을 참고하여 설정하고 수행하면 정상적으로 수행되는 것을 확인할 수 있다.

상세 설명

  1. Entities
  2. Entity Operation
  3. Association Mapping
  4. Query Language
  5. Native SQL
  6. Concurrency
  7. Cache Handling
  8. Fetch Strategy
  9. Spring Integration
  10. JPA Configuration

예제

ORM 예제

5.24 - Entities

ORM 서비스를 구성하는 가장 기초적인 클래스로 어플리케이션에서 다루고자 하는 테이블에 대응하여 구성할 수 있으며 테이블이 포함하는 컬럼에 대응한 속성들을 가지고 있다.

Entities

ORM 서비스를 구성하는 가장 기초적인 클래스로 어플리케이션에서 다루고자 하는 테이블에 대응하여 구성할 수 있으며 테이블이 포함하는 컬럼에 대응한 속성들을 가지고 있다.

기본 필요 요건

Entity를 구성하기 위한 아래와 같은 요건이 있다.(JPA요건)

[필수] Entity annotation 선언 필요 ( 혹은 XML 설정파일에 정의 )

@Entity
public class User {
}

[필수] Argument 없는 생성자 필요

public User(){
}

[필수] 최상위레벨 클래스로 생성되어야 하고 enum,interface로 정의될 수 없음

[필수] final 클래스로 선언될 수 없음

[필수] Primary Key 있어야 함 : @Id라는 Annotation 표기

@Id
private String userId;

[권장] Serializable 인터페이스 구현

public class User implements Serializable {
   private static final long serialVersionUID = -8077677670915867738L;
}

[권장] 속성 정보 접근을 위한 getter, setter 정의

private String userName;
 
public String getUserName() {
    return userName;
}

public void setUserName(String userName) {
    this.userName = userName;
}

주요 Annotations

Entity를 구성하는 주요한 Annotation은 다음과 같다.

@Entity

해당 클래스가 Entity 클래스임을 표시하는 것으로 클래스 선언문 위에 기재한다. 테이블명과 Entity명이 다를 때에는 name에 해당 테이블명을 기재한다

@Entity(name="USER_TB")
public class User {
}

@Id

해당 Attribute가 Key임을 표시하는 것으로 Attribute 위에 기재한다.

@Id
private String userId;

@Column

해당 Attribute와 매핑되는 컬럼정보를 입력하기 위한 것으로 Attribue위에 기재한다. 컬럼명과 Attribute명이 일치할 경우는 기재하지 않아도 됨

@Column(name = "DEPT_NAME", length = 30)
private String deptName;

@OneToOne, @OneToMany, @ManyToOne, @ManyToMany

테이블간 관계를 구성하기 위한 것으로 정의되는 Attribute위에 기재한다. 각각은 1:1,1:N,N:1,N;N의 관계를 표현함. 이에 대한 자세한 설명은 association_mapping 참고

@ManyToMany
private Set<Role> roles = new HashSet(0);

@Transient

테이블의 컬럼과 매핑되지 않고 쓰이는 Attribute를 정의하고자 할때 Attribute위에 기재한다.

@Transient
private String roleName;

Entity Status

  • New(transient) : 단순히 Entity 객체가 초기화되어 있는 상태를 말한다.
  • Managed(persistent) : Entity Manager에 의해 Entity가 관리되는 상태를 말한다.
  • Detached : Entity 객체가 더 이상 Persistance Context와 연관이 없는 상태이다.
  • Removed : Managed 되어 있는 Entity 객체가 삭제된 상태이다.

5.25 - Entity Operation

ORM서비스를 이용하여 특정 DB에 데이터를 입력, 수정, 조회, 삭제, 배치입력하는 방법에 대해 알아보도록 한다.

Entity Operation

ORM서비스를 이용하여 특정 DB에 데이터를 입력, 수정, 조회, 삭제, 배치입력하는 방법에 대해 알아보도록 한다.

입력

EntityManager의 persist()메소드를 호출하여 DB에 단건의 데이터를 추가할 수 있다. persist() 메소드 호출시 대상이 되는 Entity를 입력 인자로 전달해야 한다.

Sample Source

private Department addDepartment() throws Exception {
   // 1. insert a new Department information
   Department department = new Department();
   String DepartmentId = "DEPT-0001";
   department.setDeptId(DepartmentId);
   department.setDeptName("SaleDept");
   department.setDesc("판매부서");
 
   em.persist(department);
   ...
   return department;
}

위의 예를 보면 EntityManager의 persist() 메소드에 department라는 Entity를 입력인자로 전달하여 처리하였다.

수정

수정을 하고자 할때는 두가지 방법으로 가능한데 우선 EntityManager의 merge()메소드를 호출하여 DB에 단건의 데이터를 수정할 수 있고, 특정 객체가 Persistent 상태이고, 동일한 트랜잭션 내에서 해당 객체의 속성 값에 변경이 발생한 경우 merge() 메소드를 직접적으로 호출하지 않아도 트랜잭션 종료 시점에 변경 여부가 체크되어 변경 사항이 DB에 반영된다.

merge 호출 Sample Source

public void testUpdateDepartment() throws Exception {
   // 1. insert a new Department information
   Department department = addDepartment();
 
   // 2. update a Department information
   department.setDeptName("Purchase Dept");
 
   // 3. 명시적인 메소드 호출
   em.merge(department);
}

위의 예를 보면 EntityManager의 merge() 메소드에 department라는 Entity를 입력인자로 전달하여 처리하였다.

merge 호출없는 Sample Source

public void testUpdateDepartment() throws Exception {
   // 1. insert a new Department information
   Department department = addDepartment();
 
   // 2. update a Department information
   department.setDeptName("Purchase Dept");
 
   // commit. successful update!!!
}

위의 예를 보면 setDeptName()을 통해서 변경하고 Commit 처리시 변경된다.

조회

EntityManager의 find()메소드를 호출하여 DB에서 원하는 한건의 데이터를 조회할 수 있다. find() 메소드 호출시 대상이 되는 Entity의 Id를 입력 인자로 전달해야 한다.

Sample Source

private void assertDepartmentInfo(String departmentId, Department department)
              throws Exception {
 
   Department result = (Department) em.find(Department.class, departmentId);
 
   //...
}

위의 예를 보면 EntityManager의 find() 메소드에 departmentId라는 Entity Id를 입력인자로 전달하여 처리하였다.

삭제

EntityManager의 remove()메소드를 호출하여 DB에서 원하는 한건의 데이터를 조회할 수 있다. remove() 메소드 호출시 대상이 되는 Entity를 입력 인자로 전달해야 한다.

Sample Source

public void testDeleteDepartment() throws Exception {
 
   // 1. insert a new Department information
   Department department = addDepartment();
 
   // 2. delete a Department information
   em.remove(department);
 
}

위의 예를 보면 EntityManager의 remove() 메소드에 department라는 Entity를 입력인자로 전달하여 처리하였다. 그러나 주의할 점은 위의 예에서는 department 객체는 DB에 등록처리한 객체와 동일 객체이기에 그대로 remove를 쓸 수 있지만 키만 동일하게 신규로 객체를 만든경우에는 remove를 바로 쓸수 없다. 그럴 경우에는 아래와 같은 방법으로 처리해야 한다.

Sample Source

public void testDeleteDepartment() throws Exception {
 
   Department department = new Department();
   department.setDeptId = "DEPT_1";
 
   // 2. delete a Department information
   em.remove(em.getReference(Department.class, department.getDeptId()));
 
}

위의 예에서는 getReference 메소드를 호출하여 Entity의 Id에 해당하는 객체 정보를 추출하여 그정보를 입력인자로 해서 remove를 호출하였다.

배치입력

EntityManager의 persist()메소드를 호출하여 DB에 입력하고 loop를 통해 반복적으로 수행한다. OutOfMemoryException 방지를 위해서 일정한 term을 두고 flush(),clear()을 호출하여 메모리에 있는 사항을 삭제한다.

Sample Source

public void testMultiSave() throws Exception {
 
   for (int i = 0; i < 900 ; i++) {
 
      Department department = new Department();
      String DeptId = "DEPT-000" + i;
      department.setDeptId(DeptId);
      department.setDeptName("Sale" + i);
      department.setDesc("판매부" + i);
 
      em.persist(department);
      logger.debug("=== DEPT-000"+i+" ===");
 
      // OutOfMemoryException 피하기 위해서
      if (i != 0 && i % 9 == 0) {
         em.flush();
         em.clear();
     }
  }
}

Callback Methods

엔티티 클래스에 정의하거나 EntityListener를 지정하여 콜백함수를 정의하여 실제 엔티티 Operation 직전 직후에 비지니스 로직 체크등의 로직을 별도 분리하여 처리하도록 지원한다.

Callback Methods

메소드명설 명관련 Operation
PrePersistPersist이전 시점에 수행persist
PostPersistPersist이후 시점에 수행persist
PreRemoveRemove이전 시점에 수행remove
PostRemoveRemove이후 시점에 수행remove
PreUpdateUpdate이전 시점에 수행merge
PostUpdateUpdate이후 시점에 수행merge
PostLoadFind 이후 시점에 수행find

Entity에 직접 정의

콜백 함수를 Entity 클래스에 직접 Annotation을 기재하여 Method를 정의할 수 있다.

Define Source - Entity 클래스에 정의

@Entity
public class User {
   @PrePersist
   @PreUpdate
   protected void validateCreate() throws Exception {
      if (getSalary() < 2000000 )
         throw new Exception("Insufficient Salary !");
   }
}

위의 예를 보면 Persist, Update 이전 시점에 Salary가 2,000,000 이하가 되는지 여부를 체크를 하도록 한다. Update의 경우는 EntityManager의 merge()를 이용하여 하는 것과 ql을 이용하여 Update 수행하는 것 모두에 해당한다.

Sample Source - merge() 이용 케이스

@Test
public void testUpdateFailUser() throws Exception {
 
   newTransaction(); 
 
   User getUser = (User) em.find(User.class, "User1");
   assertEquals(getUser.getSalary(), sal );
   user.setSalary(1000000);
 
   em.merge(user);
 
   // 2. Update User , 위의 merge() 호출이 아닌 Transaction 종료시 Update수행됨. 
   try{
      closeTransaction();
   }
      catch( Exception e ){
      e.printStackTrace();
      assertTrue("fail to PreUpdate.",e instanceof Exception);
   }
}

위의 예를 보면 salary가 2000000 이하로 설정되어 Update될 경우 Exception 이 발생하는 것을 알 수 있다.

Sample Source - ql을 통한 Update 케이스

@Test
public void testUpdateFail2User() throws Exception {
 
   newTransaction(); 
 
   User getUser = (User) em.find(User.class, "User1");
 
   StringBuffer ql = new StringBuffer();
   ql.append("UPDATE User user ");
   ql.append("SET user.salary = :salary ");
   ql.append("WHERE user.userId = :userId ");
 
   Query query = em.createQuery(ql.toString());
   query.setParameter("salary", 1000000);
   query.setParameter("userId",getUser.getUserId());
 
   // 2. Update User , 위의 merge() 호출이 아닌 Transaction 종료시 Update수행됨. 
   try{
      closeTransaction();
   }
      catch( Exception e ){
      e.printStackTrace();
      assertTrue("fail to PreUpdate.",e instanceof Exception);
   }
}

위의 예를 보면 salary가 2000000 이하로 설정되어 Update될 경우 Exception 이 발생하는 것을 알 수 있다. ql를 이용한 Update를 처리할 때도 동일하게 Exception이 발생함을 확인할 수 있다.

EntityListener 정의

엔티티 클래스에 EntityListener를 지정하고 EntityListener에서 Annotation을 기재하여 Method를 정의할 수 있다.

Define Source - EntityListener 클래스에 정의

@Entity
@EntityListeners(egovframework.sample.model.callback.AlertMonitor.class)
public class User {
}
 
//위에서 정의한 AlertMonitor 클래스
public class AlertMonitor {	
   @PostPersist
   public void newUserAlert(User user) {
      System.out.println(user.getUserName()+" Created!");
   }
 
   @PostLoad
   public void usrGetAlert(User user) {
      System.out.println(user.getUserName()+" Get!");
   }
}

정의하는 위치가 다르고 원래 엔티티를 매개변수로 넘겨야 하는 부분이 차이가 있지만 엔티티 클래스에 지정하는 것과 동일하게 작동한다.

5.26 - Association Mapping

두 클래스 간의 연관 관계를 매핑하는 방법으로, 1:1 관계는 @OneToOne 애노테이션을 사용해 처리하고, 1:N 관계는 @OneToMany와 @ManyToOne 애노테이션을 사용해 매핑한다. 다양한 컬렉션 매핑과 inverse, cascade 같은 주요 속성도 함께 설정할 수 있다.

Association Mapping

두 클래스 사이의 Association 유형별 매핑 방법에 대해서 살펴보고자 한다. 그리고 다양한 Collection의 매핑 방법 및 Collection의 주요속성인 inverse,cascade에 대해서 샘플코드를 중심으로 살펴본다.

One To One Mapping

테이블간 1:1 매핑이 있을 경우에 각각의 Entity 클래스를 정의하고 클래스간 관계를 OneToOne 매핑으로 처리한다.

Sample Source
@Entity
public class Employee {
    @OneToOne
    private TravelProfile profile;
}

@Entity
public class TravelProfile {
    @OneToOne
    private Employee employee;
}

위의 예를 보면 Employee 와 TravelProfile가 각각 OneToOne이라는 Annotation을 기재하여 매핑처리한 것을 알수 있다.

One To Many Mapping

테이블간 1:N 매핑이 있을 경우에 각각의 Entity 클래스를 정의하고 한쪽에는 OneToMany, 다른쪽에는 ManyToOne 이라는 Annotation을 기재하여 관계를 나타낸다.

Sample Source
@Entity
public class Department{
    @OneToMany(targetEntity=User.class)
    private Set<User> users = new HashSet(0);
}

@Entity
public class User{
    @ManyToOne
    private Department department;
}

위의 예를 보면 Department:User = 1:N 의 관계가 있으며 그 관계에 대해서 Department 클래스에서 OneToMany로 표시하고 User 클래스에서 ManyToOne으로 표시하여 관계를 나타냈다.

Collection Type

Collection은 위의 예에서 사용된 Set 이외에도 List,Map를 사용할 수 있는데 각각의 사용법은 다음과 같다

Set

java.util.Set 타입으로 <set>을 이용하여 정의한다. 객체의 저장 순서를 알 수 없으며, 동일 객체의 중복 저장을 허용하지 않는다. (HashSet 이용) 다음은 set 태그를 이용하여 Collection 객체를 정의한 소스 코드의 예이다

Sample Source
@Entity
public class Department{
    @OneToMany(targetEntity=User.class)
    private Set<User> users = new HashSet(0);
}

List

java.util.List 타입으로 <list>를 이용하여 정의한다. List 타입의 경우 저장된 객체의 순서를 알 수 있으며, 저장 순서를 테이블에 보관하기 위해서 별도 인덱스 컬럼 정의가 필요하다. (ArrayList 이용) 다음은 list 태그를 이용하여 Collection 객체를 정의한 소스 코드의 예이다

Sample Source
@Entity
public class Department{
    @OneToMany(targetEntity=User.class )
    private List<User> users = new ArrayList(0);
}

Map

java.util.map 타입으로 <map>을 이용하여 (키,값)을 쌍으로 정의한다. (HashMap 이용) 다음은 map 태그를 이용하여 Collection 객체를 정의한 소스 코드의 예이다. Key 설정을 위해 MapKey라는 Annotation을 추가적으로 정의해야 한다.

Sample Source
@Entity
public class Department{
    @OneToMany(targetEntity=User.class)
    @MapKey(name="userId")
    private Map<String,User> users ;
}

단방향/양방향 관계 속성

1:N(부모:자식)관계 지정에 있어서 자식쪽에서 부모에 대한 참조 관계를 가지고 있느냐 없느냐에 따라서 참조관계가 있으면 양방향 관계, 없으면 단방향 관계로 정의된다.

단방향

자식 Entity에 부모 Entity에 대한 참조정보 없이 정의한다.

Sample Source
@Entity
public class Department{
    @OneToMany(targetEntity=User.class)
    private Set<User> users = new HashSet(0);
}

@Entity
public class User{
    @Column(name="DEPT_ID")
    private String deptId;
}

위의 예에서 보면 User 클래스에서 Department 클래스에 대한 참조관계를 지정하지 않고 단순하게 테이블의 컬럼 DEPT_ID와의 매핑으로 deptId를 지정한 것을 알 수 있다.

양방향

자식 Entity에 부모 Entity에 대한 참조정보를 지정하여 정의한다.

Sample Source
@Entity
public class Department{
    @OneToMany(targetEntity=User.class)
    private Set<User> users = new HashSet(0);
}

@Entity
public class User{
    @ManyToOne
    private Department department;
}

위의 예에서 보면 User 클래스에서 Department 클래스에 대한 ManyToOne Annotation을 통해 매핑관계를 지정한 것을 알 수 있다.

mappedBy,Cascade 속성

mappedBy와 cascade는 Collection 정의시 중요한 의미를 가지는 속성 중의 하나로, 다음과 같은 의미를 지닌다.

  • mappedBy: 객체간 관계 연결을 어느 Entity에서 할 것인지에 대한 옵션을 정의하기 위한 속성이다.
  • cascade : 부모 객체에 대한 CUD를 자식 객체에도 전이할지에 대한 옵션을 정의하기 위한 속성이다.

mappedBy: ○, cascade: ○

mappedBy와 cascade를 모두 정의하여 사용할 경우에는 자식 Entity에서 관계연결 처리를하고 부모 Entity에서 CUD처리시 자식 Entity도 자동으로 처리된다.

Define Source
@Entity
public class Department{
@OneToMany(targetEntity=User.class,mappedBy="deptId",cascade={CascadeType.PERSIST, CascadeType.MERGE})
private Set<User> users = new HashSet(0);
}

@Entity
public class User{
@ManyToOne(targetEntity=Department.class)
private Department department;
}
Sample Source
// User(자식) Entity의 setDepartment 메소드를 통해서 관계설정
user.setDepartment(department);

// 부모 entity의 저장으로 자식까지 동시처리
em.persist(department);

mappedBy: ○, cascade: Ⅹ

mappedBy만 정의하여 사용할 경우에는 자식 Entity에서 관계연결 처리를하고 부모,자식 각자 CUD처리한다.

Define Source
@Entity
public class Department{
    @OneToMany(targetEntity=User.class,mappedBy="deptId")
    private Set<User> users = new HashSet(0);
}

@Entity
public class User{
    @ManyToOne(targetEntity=Department.class)
    private Department department;
}
Sample Source
// User(자식) Entity의 setDepartment 메소드를 통해서 관계설정
user.setDepartment(department);

// 부모/자식 entity의 각각 처리
em.persist(department);
em.persist(user);

mappedBy: Ⅹ, cascade: ○

cascade만 정의하여 사용할 경우에는 부모 Entity에서 관계연결 처리를 하고 부모 Entity에서 CUD처리시 자식 Entity도 자동으로 처리된다.

Define Source
@Entity
public class Department{
@OneToMany(targetEntity=User.class,cascade={CascadeType.PERSIST, CascadeType.MERGE})
private Set<User> users = new HashSet(0);
}

@Entity
public class User{
@ManyToOne(targetEntity=Department.class)
private Department department;
}
Sample Source
// Department(부모) Entity의 getUsers().add 메소드를 통해서 관계설정
department.getUsers().add(user);

// 부모 entity의 저장으로 자식까지 동시처리
em.persist(department);

mappedBy: Ⅹ, cascade: Ⅹ

모두 정의하지 않을때는 부모 Entity에서 관계 연결 처리를 하고 부모,자식 각자 CUD처리한다.

Define Source
@Entity
public class Department{
    @OneToMany(targetEntity=User.class)
    private Set<User> users = new HashSet(0);
}

@Entity
public class User{
    @ManyToOne(targetEntity=Department.class)
    private Department department;
}
Sample Source
// Department(부모) Entity의 getUsers().add 메소드를 통해서 관계설정
department.getUsers().add(user);

// 부모/자식 entity의 각각 처리
em.persist(department);
em.persist(user);

Many To Many Mapping

테이블간 M:N 매핑이 있을 경우에 각각의 Entity 클래스를 정의하고 양쪽에 ManyToMany이라는 Annotation을 기재하여 관계를 나타낸다.

Sample Source
@Entity
public class Role{
    @ManyToMany(targetEntity=User.class)
    private Set<User> users = new HashSet(0);
}

@Entity
public class User{
    @ManyToMany
    @JoinTable(name="AUTHORITY",
        joinColumns=@JoinColumn(name="USER_ID"),
        inverseJoinColumns=@JoinColumn(name="ROLE_ID"))
    private Set<Role> roles = new HashSet(0);
}

위의 예를 보면 Role:User = M:N 의 관계가 있으며 그 관계에 대해서 Role클래스에서 ManyToMany로 표시하고 User 클래스에서 ManyToMany로 표시하여 관계를 나타내면서 User 클래스에서 관계를 위한 별도의 테이블에 대한 정의를 하고 있다. 이 경우에 ROLE과 USER를 연결하는 관계 테이블로 AUTHORITY가 사용된 것을 알 수 있다.

5.27 - JPA Query Language

JPA는 객체 지향적인 관점에서 DB 유형에 독립적인 쿼리를 정의할 수 있도록 별도의 Query Language(QL)를 제공하며, 주요 유형으로 SELECT 문과 UPDATE/DELETE 문이 있다. SELECT 문은 다양한 절(WHERE, ORDER BY, GROUP BY)과 함께 사용되며, UPDATE/DELETE 문은 WHERE 절과 함께 사용된다.

Query Language

JPA에는 별도의 Query Language를 제공함으로써 객체 지향 관점에서 특정 객체에 대한 조회와 DB 유형에 독립적인 Query 정의를 가능하도록 한다. 구성요소 및 작성 방법은 아래와 같다.

구성요소

QL Statement 유형으로는 SELECT 문과 Update and Delete 문 두가지가 있다.

  • SELECT문 : SELECT 절 + FROM 절 + WHERE 절(Option) + ORDER BY 절(Option) + GROUP BY 절(Option)
  • UPDATE&DELETE문 : UPDATE/DELETE 절 + WHERE 절 각각의 절에 대해서 아래에서 알아보고자 한다.

SELECT 절

조회 결과값을 구체적으로 명시하고자 할 경우 정의한다.

SELECT [object 또는  property], Aggregate Funtions , ...

여러 건의 데이터를 조회할 경우 조회 결과값을 List, Map 또는 사용자 정의 Type으로 정의 가능하다. (Default = Object[])

SELECT new List(prop1, prop2, )

가능한 Aggregate Funtions

COUNT   : Long 으로 리턴
MAX, MIN: 정의된 필드로 리턴
AVG     : Double로 리턴
SUM     : integral type의 경우는 Long, float type의 경우는 Double, BigInteger는 BigInteger, BigDecimal 은 BigDecimal 

QL에서 사용 가능한 주요 Function

String Functions

함수명설명
CONCAT(str1, str2)두개의 문자열을 연결한다.
SUBSTRING(str, idx, length)문자열의 지정한 idx 위치에서 length만큼의 문자열을 얻어낸다.
TRIM([type] str)문자열의 앞뒤 공백을 삭제한다. (Type이 BOTH일 경우 앞뒤공백 삭제, Type이 LEADING일 경우 앞 공백 삭제, Type이 TRAILING일 경우 뒤 공백 삭제)
LOWER(str)소문자로 변환한다.
UPPER(str)대문자로 변환한다.
LENGTH(str)문자열의 전체 길이를 구한다.
LOCATE(str, s, idx)해당 문자열 str에서 정의된 문자열 s가 포함되어 있는 위치를 구한다. 검색 시작 위치는 idx이다.

Arithmetic Functions

함수명설 명
ABS(num)숫자의 절대값을 구한다.
SQRT(num)숫자의 제곱근을 구한다.
MOD(num1,num2)num2을 num2로 나눈 나머지값을 구한다.
SIZE(collection value)Collection의 포함 엔트리 숫자를 구한다.

DateTime Functions

함수명설 명
CURRENT_DATE현재 날짜를 구한다.
CURRENT_TIME현재 시간을 구한다.
CURRENT_TIMESTAMP현재 날짜 및 시간을 구한다.

FROM 절

조회 대상 객체를 정의하며, SELECT 절이 생략되었을 경우 FROM 절에 정의된 객체가 전달 대상이 된다.

FROM [object] ((AS) alias), 

JOINS

FROM 절에 JOIN 을 쓸 수 있는데 JOIN의 종류는 다음과 같다.

JOIN 종류예 제설 명
Inner JoinsSELECT c FROM Customer c JOIN c.orders o WHERE c.status = 1비교 대상 양쪽 모두 존재하는 것 추출(Order를 낸 고객만 추출)
Left Outer JoinsSELECT c FROM Customer c LEFT JOIN c.orders o WHERE c.status = 1한쪽에만 존재하더라도 추출(Order를 내지않은 고객도 추출)
Fetch JoinsSELECT d FROM Department d LEFT JOIN FETCH d.employees WHERE d.deptno = 1FETCH문 뒤에 오는 자식리스트도 같이 추출 (Department의 attribute로 가지고 있는 employee 목록 추출, lazy로딩의 경우 Fetch를 하지 않으면 employee정보는 추출되지 않음)

WHERE 절(Option)

조회 결과 영역을 보다 상세히 구분하고자 할 경우 정의한다.

WHERE [condition], 

조건을 나타내는 여러 표현이 있는데 그중에 대표적인 것은 다음과 같다. Outer pipes Cell padding No sorting

조건설명
Path Expressionsentity 클래스의 attribute를 지칭함.user.roles
Named Parameters이름을 지정한 파라미터를 지정할 수 있고 setParameter를 통해서 값을 지정함.WHERE department.deptName like :condition
Positional Parameters위치를 지정한 파라미터를 지정할 수 있고 setParameter를 통해서 값을 지정함.WHERE role.roleName = ?1
Collection Member ExpressionsCollection 타입의 Attribute를 ”[NOT] MEMBER [OF]” 라는 표현으로 조건처리함.user MEMBER OF role.users

그외에도 IN, LIKE, IS NULL, EXISTS, Function 등의 표현이 지원된다.

ORDER BY 절(Option)

조회 결과의 정렬 방법을 정의한다.

ORDER BY [condition] (ASC 또는 DESC), 

GROUP BY 절(Option)

조회 결과를 특정 기준으로 그룹핑하고자 할 경우 정의한다

GROUP BY [condition], 
[HAVING] [condition]

기본적인 사용 방법

대표적인 사용 방법을 예제소스 기반으로 설명하고자 한다. 기본적인 CRUD 방법과 Join 방법은 다음과 같다

기본 리스트 조회

QL을 통해 하나의 테이블을 대상으로 조회 작업을 수행할 수 있다.

Sample Source

   StringBuffer qlBuf = new StringBuffer();
   qlBuf.append("FROM Department department ");
   qlBuf.append("WHERE department.deptName like :condition ");
   qlBuf.append("ORDER BY department.deptName");
 
   Query qlQuery = em.createQuery(qlBuf.toString());
   qlQuery.setParameter("condition", "%%");
 
   List departmentList = qlQuery.getResultList();

위와 같이 정의된 QL문을 통해 조회 조건에 맞는 Department 객체의 List가 리턴된다. WHERE절의 조회 조건은 객체명.Attribute명(department.deptName)으로 정의할 수 있으며 ‘:‘을 사용하여 Named Paramenter를 통해 조회 조건을 완성할 수 있다. 조회 조건의 값은 Query의 setParameter() 메소드를 통해 지정해 주고 있다.

JOIN을 통한 리스트 조회

INNER JOIN 과 LEFT OUTER JOIN 을 수행할 수 있고 그 예는 다음과 같다.

INNER JOIN (1)

   StringBuffer qlBuf = new StringBuffer();
 
   qlBuf.append("SELECT user ");
   qlBuf.append("FROM User user join user.roles role ");
   qlBuf.append("WHERE role.roleName = ?1");
 
   Query query = em.createQuery(qlBuf.toString());
   query.setParameter(1, "Admin");
 
   List userList = query.getResultList();

위와 같이 정의된 QL문을 통해 조회 조건에 맞는 Department 객체의 List가 리턴된다. FROM절에서 JOIN을 이용하여 INNER JOIN 처리를 했고 WHERE절의 조회 조건은 객체명.Attribute명(department.deptName)으로 정의할 수 있으며 ‘?!‘를 사용하여 Positional Paramenter를 통해 조회 조건을 완성할 수 있다. 조회 조건의 값은 Query의 setParameter() 메소드를 통해 지정해 주고 있다.

INNER JOIN (2)

   StringBuffer qlBuf = new StringBuffer();
 
   qlBuf.append("SELECT distinct user ");
   qlBuf.append("FROM User user, Department department ");
   qlBuf.append("WHERE user.department.deptId = department.deptId ");
   qlBuf.append("AND department.deptId = :condition1 ");
   qlBuf.append("AND user.userName like :condition2 ");
 
   Query query = em.createQuery(qlBuf.toString());
   query.setParameter("condition1", "Dept1");
   query.setParameter("condition2", "%%");
 
   List userList = query.getResultList();

위와 같이 정의된 QL문을 통해 조회 조건에 맞는 Department 객체의 List가 리턴된다. WHERE절에서 ‘=‘를 통해 INNER JOIN 처리를 했고 조회 조건은 객체명.Attribute명(department.deptName)으로 정의할 수 있으며 ‘?!‘를 사용하여 Positional Paramenter를 통해 조회 조건을 완성할 수 있다. 조회 조건의 값은 Query의 setParameter() 메소드를 통해 지정해 주고 있다.

LEFT OUTER JOIN

   StringBuffer qlBuf = new StringBuffer();
 
   qlBuf.append("SELECT distinct role ");
   qlBuf.append("FROM Role role left outer join role.users user ");
   qlBuf.append("ORDER BY role.roleName ASC ");
 
   Query query = em.createQuery(qlBuf.toString());
 
   List roleList = query.getResultList();

위와 같이 정의된 QL문을 통해 조회 조건에 맞는 role객체의 List가 리턴된다. FROM절에서 LEFT OUTER JOIN 처리를 했다. LEFT OUTER JOIN이므로 RIGHT에 있는 정보가 누락되더라도 추출되므로 위의 예에서는 USER 정보가 없는 ROLE 정보도 모두 리스트 됨을 알 수 있다.

Defined Return Type

조회 작업을 수행한 후, 조회 작업의 결과를 원하는 객체 형태로 전달받을 수 있다. 이는 여러 테이블을 Join할 경우 한 테이블에 매핑되는 Persistence 클래스가 아닌 composite 클래스로 리턴받고자 할 때 사용할 수 있다.

특정 객체 형태로 전달

Relation 관계에 놓여 있는 두개의 테이블을 대상으로 QL(Inner Join)을 이용한 조회 결과를 특정 객체(예에선 User객체)형태로 전달받는다

   StringBuffer qlBuf = new StringBuffer();
   qlBuf.append("SELECT new User(user.userId as userId, ");
   qlBuf.append("	user.userName as userName, user.password as password, ");
   qlBuf.append("	role.roleName as roleName, ");
   qlBuf.append("	user.department.deptName as deptName) ");
   qlBuf.append("FROM User user join user.roles role ");
   qlBuf.append("WHERE role.roleName = :condition");
 
   Query query = em.createQuery(qlBuf.toString());
   query.setParameter("condition", "Admin");
 
   List userList = query.getResultList();

한가지 주의할 점은 new User(…)를 통해서 생성자를 호출하고 있는데 이 생성자가 User 클래스에 정의되어 있어야 한다. 또한 리턴된 결과값에서 각각의 attribute에 해당하는 값을 꺼낼 때에는 List에서 각 User 객체를 꺼낸 다음 getter 메소드를 사용한다.

   User user1 = (User) userList.get(0);
   user1.getUserName());
   User user2 = (User) userList.get(1);
   user2.getUserName());

Map 형태로 전달

Relation 관계에 놓여 있는 두개의 테이블을 대상으로 QL(Inner Join)을 이용한 조회 결과를 Map 형태로 전달받는다

   StringBuffer qlBuf = new StringBuffer();
 
   qlBuf.append("SELECT new Map(user.userId as userId, ");
   qlBuf.append("	user.userName as userName, user.password as password, ");
   qlBuf.append("	role.roleName as roleName, ");
   qlBuf.append("	user.department.deptName as deptName) ");
   qlBuf.append("FROM User user join user.roles role ");
   qlBuf.append("WHERE role.roleName = :condition");
 
   Query query = em.createQuery(qlBuf.toString());
   query.setParameter("condition", "Admin");
 
   List userList = query.getResultList();

위와 같이 정의할 경우 조회 결과는 Map의 List 형태가 된다. 이때 alias로 정의한 userId, userName, password, roleName, deptName이 Map의 Key 값이 된다. 따라서 다음과 같이 Map으로 정의된 Key 값을 통해 결과값을 조회할 수 있다.

   List userList = query.getResultList();
 
   Map user1 = (Map) userList.get(0);
   user1.get("userId");
   user1.get("userName");
   ...

List 형태로 전달

Relation 관계에 놓여 있는 두개의 테이블을 대상으로 QL(Inner Join)을 이용한 조회 결과를 List 형태로 전달받는다

   StringBuffer qlBuf = new StringBuffer();
 
   qlBuf.append("SELECT new List(user.userId as userId, ");
   qlBuf.append("	user.userName as userName, user.password as password, ");
   qlBuf.append("	role.roleName as roleName, ");
   qlBuf.append("	user.department.deptName as deptName) ");
   qlBuf.append("FROM User user join user.roles role ");
   qlBuf.append("WHERE role.roleName = :condition");
 
   Query query = em.createQuery(qlBuf.toString());
   query.setParameter("condition", "Admin");
 
   List userList = query.getResultList();

위와 같이 정의할 경우 조회 결과는 List의 List 형태가 된다. List에서 결과값을 꺼낼 때에는 정의된 순서에 따르면 된다.

   List userList = query.getResultList();
 
   List user1 = (List) userList.get(0);
   user1.get(1); //userId
   user1.get(2); //userName
   ...

Named Query

Entity 클래스 파일 내에 Annotation으로 정의한 QL문의 name을 입력하여 실행시킬 수 있다.

Sample Source

   Query qlQuery = em.createNamedQuery("findDeptList");
   qlQuery.setParameter("condition", "%%");
 
   List deptList = qlQuery.getResultList();

위와 같이 createNamedQuery() 메소드에 query name을 넘겨주면 이 이름에 맞는 QL문을 찾아서 실행하게 된다. 다음은 findDeptList가 담겨있는 Department Entity 클래스 소스 일부이다.

Entity Source

@Entity
@NamedQuery(name = "findDeptList", 
           query = "FROM Department department WHERE department.deptName like :condition ORDER BY department.deptName")
public class Department implements Serializable {
...
}

Paging 처리

Paging 처리는 한 페이지에 보여줘야 할 조회 목록에 제한을 둠으로써 DB 또는 어플리케이션 메모리의 부하를 감소시키고자 하는데 목적이 있다. QL 수행시 페이징 처리된 조회 결과를 얻기 위한 방법에 대해 알아보도록 한다. 특정 테이블을 대상으로(예에서는 USER 테이블) QL을 이용한 조회 작업을 수행한다. 이때, 조회를 시작해야 하는 Row의 Number(FirstResult)와 조회 목록의 개수(MaxResult)를 정의함으로써, 페이징 처리가 가능해진다.

Sample Source

   StringBuffer qlBuf = new StringBuffer();
 
   qlBuf.append("FROM User user ");
   Query query = em.createQuery(qlBuf.toString());
   //첫번째로 조회해야 할 항목의 번호
   query.setFirstResult(1);
   //조회 항목의 전체 개수
   query.setMaxResults(2);
 
   List userList = query.getResultList();

위와 같이 정의할 경우 QL에서는 persistence.xml 파일에 정의된 hibernate.dialect 속성에 따라 각각의 DB에 맞는 SQL을 생성한다. 이는 Pagination을 할 때 모든 데이터를 읽은 후 해당 페이지에 속한 데이터 갯수를 결과값으로 전달하는 것이 아니라 조회해야 할 데이터 즉, 해당 페이지에 속한 갯수만큼의 데이터만 읽어오게 된다.

QL을 이용한 CUD

기본적으로 JPA를 이용한 CUD(Create, Update, Delete)를 할 때에는 기본 API를 사용하게 된다. (본메뉴얼 Basic CRUD 참고) 그러나 특이한 경우 QL을 통해 기본 CUD를 수행해야 하는 경우가 발생할 수 있다.

INSERT

다음은 QL을 사용한 Insert문의 예이다.

   StringBuffer ql = new StringBuffer();
 
   ql.append("INSERT INTO Department (deptId,deptName) ");
   ql.append("SELECT CONCAT(deptId,'UPD'), CONCAT(deptName,'UPD') ");
   ql.append("FROM Department department ");
   ql.append("WHERE deptId = :deptId");
 
   Query query = em.createQuery(ql.toString());
   query.setParameter("deptId", "Dept1");
 
   query.executeUpdate();

위와 같이 작성할 경우 QL을 이용하여 신규 Department 정보를 등록할 수 있다.

UPDATE

다음은 QL을 사용한 Update문의 예이다.

   StringBuffer ql = new StringBuffer();
 
   ql.append("UPDATE Department department ");
   ql.append("SET department.desc = :desc ");
   ql.append("WHERE department.deptId = :deptId and department.deptName = :deptName ");
 
   Query query = em.createQuery(ql.toString());
   query.setParameter("desc", "Human Resource");
   query.setParameter("deptId", "Dept1");
   query.setParameter("deptName", "HRD");
 
   query.executeUpdate();

위의 예는 QL을 사용하여 Department 정보를 수정한 것이며 Query의 setParameter() 메소드를 통해 인자값을 세팅하고 있다.

DELETE

다음은 QL을 사용한 Delete문의 예이다.

   StringBuffer ql = new StringBuffer();
   ql.append("DELETE Department department ");
   ql.append("WHERE department.deptId = :deptId ");
 
   Query query = em.createQuery(ql.toString());
   query.setParameter("deptId", "Dept1");
 
   query.executeUpdate();

위의 예는 QL을 사용하여 Department 정보를 삭제한 것이며 Query의 setParameter() 메소드를 통해 인자값을 세팅하고 있다.

5.28 - Native SQL

JPA는 기본 API나 QL 외에도 특정 DBMS 기능을 활용하기 위해 createNativeQuery() 메소드를 통해 Native SQL 실행을 지원한다. 이를 통해 표준 SQL을 직접 사용하여 CRUD 작업을 수행할 수 있다.

Native SQL

기본적으로 CRUD 작업을 할 때 JPA 기본 API를 사용하거나 QL을 이용하여 수행한다. 그러나 특정 DBMS에서 제공하는 기능을 사용할 수 있도록 하기 위해 Native SQL 사용을 지원한다.

기본적인 사용 방법

entityManager.createNativeQuery() 메소드를 이용하여 Native SQL을 실행할 수 있다.

기본 리스트 조회

SQL을 통해 하나의 테이블을 대상으로 조회 작업을 수행할 수 있다.

Sample Source

   StringBuffer qlBuf = new StringBuffer();
 
   qlBuf.append("SELECT * ");
   qlBuf.append("FROM DEPARTMENT ");
   qlBuf.append("WHERE DEPT_NAME like :condition ");
   qlBuf.append("ORDER BY DEPT_NAME");
 
   Query query = em.createNativeQuery(qlBuf.toString(),Department.class);
   query.setParameter("condition", "%%");
 
   List deptList = query.getResultList();

위와 같이 정의된 SQL문을 통해 조회 조건에 맞는 Department 객체의 List가 리턴된다. WHERE절에서 ‘:‘을 사용하여 Named Paramenter를 통해 조회 조건을 완성할 수 있다. 조회 조건의 값은 Query의 setParameter() 메소드를 통해 지정해 주고 있다. 또한, createNativeQuery의 두번재 인자로 리턴받고자하는 Entity 클래스(Department.class)를 지정한 것을 확인할 수 있다.

JOIN을 통한 리스트 조회

Relation 관계에 놓여 있는 두개의 테이블을 대상으로 Native SQL(Inner Join)을 이용한 조회 작업을 수행할 수 있다.

Basic Source

   StringBuffer qlBuf = new StringBuffer();
 
   qlBuf.append("SELECT user.* ");
   qlBuf.append("FROM USER user ");
   qlBuf.append("join AUTHORITY auth on user.USER_ID = auth.USER_ID ");
   qlBuf.append("join ROLE role on auth.ROLE_ID = role.ROLE_ID ");
   qlBuf.append("WHERE role.ROLE_NAME = ?");
 
   Query query = em.createNativeQuery(qlBuf.toString(),User.class);
   query.setParameter(1, "Admin");
 
   List userList = query.getResultList();

위의 코드와 같이 join 키워드를 사용하여 Inner Join을 수행할 수 있다. 또한, Relation 관계에 놓여 있는 두개의 테이블을 대상으로 Native SQL(Right Outer Join)을 이용한 조회 작업을 수행할 수 있다.

RIGHT JOIN Source

   StringBuffer qlBuf = new StringBuffer();
 
   qlBuf.append("SELECT distinct role.* ");
   qlBuf.append("FROM USER user ");
   qlBuf.append("right join AUTHORITY auth on user.USER_ID=auth.USER_ID ");
   qlBuf.append("right join ROLE role on auth.ROLE_ID=role.ROLE_ID ");
   qlBuf.append("ORDER BY role.ROLE_NAME ASC ");
 
   Query query = em.createNativeQuery(qlBuf.toString(),Role.class);
 
   List roleList = query.getResultList();

또한 Join하여 조회한 결과를 각각의 Join된 객체의 값으로 select 하기 위해서는 createNativeQuery의 두번째 인자에 @SqlResultSetMapping에 정의된 명을 기재하여 수행한다.

Multi Entity Result Source

   StringBuffer qlBuf = new StringBuffer();
 
   qlBuf.append("SELECT distinct user.*, department.* ");
   qlBuf.append("FROM USER user, DEPARTMENT department ");
   qlBuf.append("WHERE user.DEPT_ID = department.DEPT_ID ");
   qlBuf.append("AND department.DEPT_NAME = :condition1 ");
   qlBuf.append("AND user.USER_NAME like :condition2 ");
 
   Query query = em.createNativeQuery(qlBuf.toString(), "UserAndDept" ) ;		
   query.setParameter("condition1", "HRD");
   query.setParameter("condition2", "%%");
 
   List userList = query.getResultList();

위의 예를 보면 User Entity Class에 UserAndDept 라는 이름으로 리턴받고자 하는 Entity 클래스를 정의하고 있음을 알 수 있다.

SqlResultSetMapping Define Source

@SqlResultSetMapping(name="UserAndDept",entities={@EntityResult(entityClass=User.class),
		                                  @EntityResult(entityClass=Department.class)
		                                 }
                    )
@Entity
public class User implements Serializable {
}

위의 예를 보면 User Entity 클래스에서 Annotation을 통해서 UserAndDept를 정의하고 있음을 알 수 있다. 또한, 각각의 추출은 아래와 같이 한다.

Multi Entity Result Use Source

   Object[] results = (Object[]) userList.get(0);
 
   User user1 = (User)results[0];
   Department dept1 = (Department)results[1];

Named Query

Entity 클래스 파일 내에 Annotation으로 정의한 SQL문의 name을 입력하여 실행시킬 수 있다.

Sample Source

   Query query = em.createNamedQuery("nativeFindDeptList");
   query.setParameter("condition", "%%");
 
   List deptList = query.getResultList();

위와 같이 createNamedQuery() 메소드에 query name을 넘겨주면 이 이름에 맞는 QL문을 찾아서 실행하게 된다. 다음은 nativeFindDeptList가 담겨있는 Department Entity 클래스 소스 일부이다.

Entity Source

@Entity
@NamedNativeQuery(name="nativeFindDeptList",
           resultClass=Department.class,
	         query="SELECT * FROM DEPARTMENT department " +
		       "WHERE department.DEPT_Name like :condition "+
		       "ORDER BY department.DEPT_Name" )
public class Department implements Serializable {
...
}

위에서 볼 수 있듯이 QL의 NamedQuery과는 resultClass 를 명시적으로 기재한다는 점에서 차이가 있다.

Paging 처리

Paging 처리는 한 페이지에 보여줘야 할 조회 목록에 제한을 둠으로써 DB 또는 어플리케이션 메모리의 부하를 감소시키고자 하는데 목적이 있다. Native SQL 수행시 페이징 처리된 조회 결과를 얻기 위한 방법에 대해 알아보도록 한다. 특정 테이블을 대상으로(예에서는 USER 테이블) Native SQL을 이용한 조회 작업을 수행한다. 이때, 조회를 시작해야 하는 Row의 Number(FirstResult)와 조회 목록의 개수(MaxResult)를 정의함으로써, 페이징 처리가 가능해진다.

Sample Source

   StringBuffer qlBuf = new StringBuffer();
 
   qlBuf.append("SELECT * ");
   qlBuf.append("FROM User ");
   Query query = em.createNativeQuery(qlBuf.toString(),User.class);
 
   query.setFirstResult(1);
   query.setMaxResults(2);
 
   List userList = query.getResultList();

위와 같이 정의할 경우 QL에서는 persistence.xml 파일에 정의된 hibernate.dialect 속성에 따라 각각의 DB에 맞는 SQL을 생성한다. 이는 Pagination을 할 때 모든 데이터를 읽은 후 해당 페이지에 속한 데이터 갯수를 결과값으로 전달하는 것이 아니라 조회해야 할 데이터 즉, 해당 페이지에 속한 갯수만큼의 데이터만 읽어오게 된다.

Function 호출

해당 DB에 생성한 Function을 이용하여 Native SQL을 실행하고 결과를 확인할 수 있다.

Sample Source

   StringBuffer qlBuf = new StringBuffer();
 
   qlBuf.append("SELECT * FROM USER_TBL ");
   qlBuf.append("WHERE salary > FIND_USER(:condition)");
 
   Query query = em.createNativeQuery(qlBuf.toString(),User.class);
   query.setParameter("condition", "User1");
 
   List userList = query.getResultList();

위의 예에서 보면 FIND_USER라는 함수를 호출하여 WHERE문에서 비교를 수행하는 것을 알 수 있다. Procedure의 경우는 입력/출력 인자 처리를 어찌 해야하는지에 대한 확인이 불가능해서 예제로 설명하지 못했다.

5.29 - Concurrency

동시에 동일한 데이터에 접근할 때 Optimistic Locking을 지원하며, Pessimistic Locking은 JPA 2.0부터 Hibernate의 Native API를 통해 지원된다.

Concurrency

동시에 동일한 데이터에 접근할 때에 데이터에 대한 접근을 제어하기 위해 Optimistic Locking을 지원한다. 한편 Hibernate의 Native API를 통해서는 지원 가능한 Pessimistic Locking 은 JPA2.0 버전에 정의될 예정이다.

Optimistic Locking

Without Locking Source

@Test
public void testUpdateUserWithoutOptimisticLocking() throws Exception {
 
   // 1. 테스트를 위한 신규 데이터를 입력
   newTransaction();
   addDepartmentUserAtOnce();
   closeTransaction();
 
   // 2. 동일한 식별자를 이용하여 User 정보를 두번 조회
   newTransaction();
   User fstUser = (User) em.find(User.class,"User1");
   User scdUser = (User) em.find(User.class,"User1");
   closeTransaction();
 
   // 3. Detached 상태에서의 변경처리
   fstUser.setUserName("First : Kim");
 
   // 4. 별도의 트랜잭션으로 변경처리
   newTransaction();
   scdUser.setUserName("Second : Kim");
   closeTransaction();
 
   // 5. 3에서 작업한 내용이 반영되어 변경.
   newTransaction();
   em.merge(fstUser);
   closeTransaction();
}

위에서 제시한 로직에 대해 자세히 살펴보자.

  1. #1, #2번 코드에 의해 각각 동일한 식별자를 이용하여 같은 데이터 조회
  2. 두번째 트랜잭션이 종료된 후, #3번 코드에서는 Detached 상태의 fstUser 객체의 userName 변경
  3. 세번째 트랜잭션 내의 #4번 코드에서는 scdUser 객체의 userName 변경, 세번째 트랜잭션 종료시 변경 사항이 DB에 반영
  4. 네번째 트랜잭션 내에서 #3번 코드를 통해 변경된 fstUser 객체에 대해 update 수행
  5. fstUser에 대한 수정 작업 또한 성공적으로 처리

결론적으로 보면, userId가 “User1”인 User의 userName은 “First : Kim”이 되어 앞서 scdUser에서 요청했던 수정 작업은 무시된 것이다. 이러한 현상을 Lost Update라고 하며, 이를 해결하기 위한 방법은 3가지가 있다.

  1. Last Commit Wins : Optimistic Locking 을 수행하지 않게 되면 기본적으로 수행되는 유형으로 2개의 트랜잭션 모두 성공적으로 commit된다. 그러므로 두번째 commit은 첫번째 commit 내용을 덮어쓸 수 있다. (위의 예의 경우)
  2. First Commit Wins : Optimistic Locking을 적용한 유형으로 첫번째 commit만이 성공적으로 이루어지며, 두번째 commit 시에는 Error를 얻게 된다.
  3. Merge : 첫번째 commit만이 성공적으로 이루어지며, 두번째 commit 시에는 Error를 얻게 된다. 그러나 First Commit Wins와는 달리 두번째 commit을 위한 작업을 처음부터 다시 하지 않고 개발자의 선택에 의해 선택적으로 변경될 수 있도록 한다. 가장 좋은 전략이나 변경 사항을 merge 할 수 있는 화면이나 방법을 직접 제공해 줄 수 있어야 한다.(추가 구현 필요함)

JPA에서는 Versioning 기반의 Automatic Optimistic Locking을 통해 First Commit Wins 전략을 취할 수 있도록 지원한다. JPA에서 Optimistic Locking을 수행하기 위해서는 해당 테이블에 Version을 추가해야 한다. 그러한 경우 해당 테이블과 매핑된 객체를 로드할 때 Version 정보도 함께 로드되고 객체 수정시 테이블의 현재 값과 비교하여 처리 여부를 결정하게 된다.

With Optimistic Locking Source

@Test
public void testUpdateDepartmentWithOptimisticLocking() throws Exception {
 
   // 1. 테스트를 위한 신규 데이터를 입력
   newTransaction();
   addDepartmentUserAtOnce();
   closeTransaction();
 
   // 2. Department 정보를 두번 조회
   newTransaction();
   Department fstDepartment = (Department) em.find(Department.class,"Dept1");
   assertEquals("fail to check a version of department.", 0, fstDepartment.getVersion());
   Department scdDepartment = (Department) em.find(Department.class,"Dept1");
   closeTransaction();
 
   // 3. 두번째 조회한 Department 정보에 다른 deptName을 셋팅하여 DB에 반영
   fstDepartment.setDeptName("First : Dept.");
 
   // 4. 첫번째 조회한 Department 정보에 대해 merge() 메소드를 호출
   newTransaction();
   scdDepartment.setDeptName("Second : Dept.");
   closeTransaction();
 
   // 5. 세번째 트랜잭션에서의 수정으로 인해 DEPARTMENT_VERSION이 이미 변경되었기 때문에
   //    StaleObjectStateException 발생이 예상
   newTransaction();
   try {
      em.merge(fstDepartment);
      closeTransaction();
   } catch (Exception e) {
      e.printStackTrace();
      assertTrue("fail to throw StaleObjectStateException.",e instanceof StaleObjectStateException);
   }
}

위와같이 다음의 testUpdateDepartmentWithOptimisticLocking() 메소드를 수행하였을 때 첫번째 수정 작업은 성공적으로 이루어지나 두번째 수정 작업에 대해서는 #6번 코드에서처럼 StaleObjectStateException이 throw될 것이다. 이를 위한 entity 클래스의 설정의 일부분은 다음과 같다.

Entity Class Source

@Entity
@Table(name="DEPARTMENT")
public class Department {
 
   private static final long serialVersionUID = 1L;
 
   @Id
   @Column(name = "DEPT_ID", length = 10)
   private String deptId;
 
   @Version
   @Column(name = "DEPT_VERSION")
   private int version;
...
}

위에서 보는 것 같이 DEPT_VERSION이라는 컬럼을 추가하여 버전관리를 하게 함으로써 Optimistic Locking처리를 할 수 있다.

5.30 - Cache Handling

JPA에서는 성능 이슈를 개선하기 위해 1레벨 캐시를 활용하며, 객체를 테이블로 매핑해 데이터 액세스 처리를 수행한다. JPA 2.0부터는 2레벨 캐시가 추가되어 더 효율적인 캐시 관리가 가능해졌다.

Cache Handling

입력 인자로 전달된 객체를 정의된 테이블로 매핑시켜 데이터 액세스 처리를 수행해야 하는데 JPA에서는 이로 인해 발생 가능한 성능 이슈를 개선하기 위해 Cache를 활용한다. 현재 표준버전에서는 1 level Cache 만을 정의하고 있다. JPA 2.0에서는 2level Cache 정의 추가됨.

1 Level Cache

Entity Manager 내부에 정의된 Cache로, 트랜잭션의 시작과 종료 사이에서 사용되며 한 트랜잭션 내에서 읽혀진 객체들을 보관하는 역할을 수행한다. JPA 구현체는 하나의 트랜잭션 내에서 동일한 객체를 한 번 이상 Loading할 경우 2번째부터는 Cache로부터 해당 객체를 추출하고 또한, 한 트랜잭션 범위 내에서 객체의 속성 변경시 변경 사항은 트랜잭션 종료시에 자동적으로 DB에 반영하도록 한다. 즉, 하나의 트랜잭션 내에서 동일한 객체에 대한 재조회가 이루어지는 경우 Cache를 이용함으로써 DB 접근 횟수를 줄여주기 때문에 어플리케이션 성능 향상에 도움이 되는 것이다.

Sample Source

@Test
public void testFindUser() throws Exception {
   newTransaction();
 
   SetUpInitCacheData.initializeData(em);
 
   User user = (User) em.find(User.class, "User1");
 
   Set roles = (Set)user.getRoles();
   roles.iterator();
 
   // 1. Read from Cache
   user = (User) em.find(User.class, "User1");
 
   roles = (Set)user.getRoles();
   roles.iterator();
 
   closeTransaction();		
}

위와 같이 작성할 경우 동일한 트랜잭션 내에서 SetUpInitCacheData.initializeData(em)을 통해 persist된 Persistence 객체는 1LC에 저장되므로 다음에 #1번 코드에서처럼 동일한 Persistence 객체 조회시 DB에 재접근하지 않고도, Cache를 통해 조회된다.

2 Level Cache

2 Level Cache에 대한 것은 JPA 2.0에서 정의하고 있어서 여기서는 JPA 구현체로 쓰이는 Hibernate에서 지원하는 방법으로 가이드를 하고자 한다. 2 Level Cache는 어플리케이션 단위의 Cache로, 어플리케이션 관점에서의 Cache 기능을 지원한다. 이는 여러 트랜잭션들을 통해 Load된 Persistence 객체를 Session Factory 레벨에서 저장하는 방법으로 처리된다. persistence.xml 파일 내에 hibernate.cache.use_second_level_cache, hibernate.cache.provider_class 등을 정의 하고, 2 Level Cache에 저장되어야 할 Persistence Class 매핑 파일의 속성을 정의하면 해당 어플리케이션을 구성하는 특정 Persistence 객체들에 대해 2LC를 적용할 수 있다. 다음은 2 Level Cache에 대한 속성이 정의되어 있는 persistence.xml 파일의 일부이다.

Config 파일

<persistence-unit name="HSQLMCUnit" transaction-type="RESOURCE_LOCAL">
   ...
   <properties>
      <property name="hibernate.cache.use_second_level_cache" value="true"/> 
      <property name="hibernate.cache.provider_class" value="org.hibernate.cache.EhCacheProvider"/> 
   ...
   </properties>
</persistence-unit>

다음은 cache 속성이 READ_WRITE로 설정되어 있는 Entity 클래스의 일부이다.

// Department에 지정
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
public class Department {
 
  // Department와 User의 Join에 지정
  @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
  @OneToMany(targetEntity=User.class, mappedBy="department" 
 	       ,cascade={CascadeType.PERSIST, CascadeType.MERGE})
  private Set<User> users ;
}
 
// User에 지정
@Entity
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
public class User {
}

위에서 Department, User , Set 세군데에 Cache를 지정한 것을 알 수 있다. 각각은 Department 와 User 그리고 Department와 User의 Join에 대해서 Cache를 지정하는 것이다. Cache 속성은 위에서 정의한 READ_WRITE외에도 다음과 값으로 정의할 수 있다.

  • READ_OLNY : Persistence 객체가 변경되지 않는 경우에 사용 가능하다. 수정이 없으므로 분산 환경에서도 안전하게 사용 가능하며 가장 빠른 성능을 제공한다.
  • NONSTRICT_READ_WRITE : 트랜잭션 격리를 엄격히 적용할 필요가 없는 경우 사용 가능하다.
  • TRANSACTIONAL : 완전한 트랜잭션을 보장하나 가장 느린 성능을 제공한다.

Sample Source

@Test
public void testFindDepartment() throws Exception {
 
   // 데이터 입력.
   newTransaction();
   SetUpInitCacheData.initializeData(em);
   closeTransaction();
 
   //Hibernate 메소드를 이용하기 위해  Hibernate Session Factory 생성(Entity Manager 로부터 얻어냄)
   newHibernateSessionFactory();
 
   // 2Level Cache를 이용한 자료 추출(Hibernate의 메소드를 이용함)
   newSession();
   Department department = (Department) session.get(Department.class, "Dept1");
 
   Set users = department.getUsers();
   users.iterator();
   closeSession();
 
   // 2Level Cache를 이용한 자료 추출(Hibernate의 메소드를 이용함)
   newSession();
   department = (Department) session.get(Department.class, "Dept1");
 
   users = department.getUsers();
   users.iterator();
 
   // Hibernate Session close
   closeSession();		
}

위의 예에서 두번째 Session.get()을 통해서는 DB 접근없이 2 Level Cache의 값을 가지고 오는 것을 알 수 있다.

5.31 - Fetch Strategy

ORM에서 기본적으로 Lazy Loading을 사용하여 필요한 시점에 SQL을 실행하지만, 성능 이슈를 해결하기 위해 Batch 조회, Sub-Query 조회, Join Fetch와 같은 다양한 Fetch 전략이 있다. 이 전략들은 JPA 표준이 아닌 Hibernate 구현체에서 제공되는 기능이다.

Fetch Strategy

ORM 서비스는 기본적으로 Entity간의 연관관계를 정의하고 정의된 연관관계를 가지고 관련 Entity를 추출하여 사용한다. 관련 Entity를 추출하는데 기본적으로 Lazy Loading이란 기법을 통해서 객체가 실제로 필요하기 전까지 SQL을 실행하지 않고 Proxy 객체로 리턴하도록 하고 한다. 그러나 이러한 Lazy Loading으로 처리하게 되면 Lazy Loading을 하지 않는 것에 대비하여 필요시점에 쿼리를 여러번 수행해야 하는 문제가 발생한다. 이런 문제를 해결하기 위한 여러가지 Fetch 전략이 존재하는데 Batch를 이용하여 데이터 조회, Sub-Query를 이용하여 데이터 조회, Join Fetch를 이용하여 데이터 한꺼번에 조회하는 방법이 있다. 하지만 이 서비스는 JPA표준이 아닌 구현체인 Hibernate에 정의된 사항이다.

Batch를 이용하여 데이터 조회

Entity 클래스에 BatchSize를 지정할 경우 지정한 개수만큼 해당 객체를 로딩하는 방식으로 쿼리 실행 회수가 n / batch size + 1로 감소한다. 다음은 batch-size 설정 예인 Department 클래스 파일의 일부이다.

Config Source

@Entity
public class Department implements Serializable {
   ...
   @org.hibernate.annotations.BatchSize(size=2)
   private Set<User> users ;
   ...
}

위에서 Department에 속한 Set 을 추출할 때 적용되는 BatchSize를 2로 지정하였다.

Sample Source

   qlBuf.append("FROM Department");
   Query query = em.createQuery(qlBuf.toString());
 
   List deptList = query.getResultList();
 
   assertEquals("fail to match the size of department list.", 3, deptList.size());
 
   for (int i = 0; i < deptList.size(); i++) {
 
      Department department = (Department) deptList.get(i);
 
      if (i == 0) {
         assertEquals("fail to match a department name.", "HRD", department.getDeptName());
 
         Set users = department.getUsers();
         assertEquals("fail to match the size of user list.", 2, users.size());
 
      } else if (i == 1) {
         assertEquals("fail to match a department name.", "PD", department.getDeptName());
 
         Set users = department.getUsers();
         assertEquals("fail to match the size of user list.", 1, users.size());
   ...

위의 Config와 Sample에 의해서 query.getResultList()에 의한 것과 department.getUsers() 호출 시 자동 생성되는 SQL Query는 다음과 같다.

Generated SQL

SELECT ... FROM USER user0_
 
SELECT ... 
FROM USER users0_ 
WHERE users0_.DEPT_ID IN (?, ?)

위에 where 절의 [in (?,?)]에서의 ?의 숫자가 BatchSize이다. 위에서 2로 설정했기에 두개 지정되어 조회된다.

Sub-Query를 이용하여 데이터 조회

Entity 클래스에 FetchMode를 SUBSELECT로 지정할 경우 Sub Query 형태의 SELECT 문이 수행되며 한번에 모두 로딩하게 된다. 다음은 SUBSELECT 설정 예인 User 클래스 파일의 일부이다.

Config Source

@Entity
public class User implements Serializable {
    ...
    @org.hibernate.annotations.Fetch(org.hibernate.annotations.FetchMode.SUBSELECT) 
    private Set<Role> roles = new HashSet(0);	
    ...
}

위에서 User에 속한 Set<Role> 을 추출할 때 적용되는 FetchMode를 SUBSELECT 로 지정하였다.

Sample Source

   qlBuf.append("FROM User");
   Query query = em.createQuery(qlBuf.toString());
   List userList = query.getResultList();
 
   assertEquals("fail to match the size of user list.", 3, userList.size());
 
   for (int i = 0; i < userList.size(); i++) {
      User user = (User) userList.get(i);
 
      if (i == 0) {
         assertEquals("fail to match a user name.", "kim" , user.getUserName());
         assertEquals("fail to match a user password.", "kim123" , user.getPassword());
 
         Set roles = user.getRoles();
         assertEquals("fail to match the size of role list.", 2 , roles.size());
      } else if (i == 1) {
         assertEquals("fail to match a user name.", "lee" , user.getUserName());
         assertEquals("fail to match a user password.", "lee123" , user.getPassword());
         ...

위의 Config와 Sample에 의해서 query.getResultList() 호출 시와 user.getRoles() 호출 시 자동 생성되는 SQL Query는 다음과 같다.

Generated SQL

SELECT ... FROM USER user0_
 
SELECT ... 
FROM AUTHORITY roles0_ LEFT OUTER JOIN ROLE role1_ ON roles0_.ROLE_ID=role1_.ROLE_ID 
WHERE roles0_.USER_ID IN (SELECT user0_.USER_ID FROM USER user0_)

위에 where절의 [in (select user0_.USER_ID from USER user0_)]를 보면 Sub Query 형태로 해당하는 모든 User에 대해서 모든 Roles 정보를 추출하는 것을 확인 할 수 있다.

join fetch를 이용하여 데이터 한꺼번에 조회하는 방법

Entity 클래스에서의 별도 설정없이 QL 수행시 join fetch 를 기재함으로써 한번에 연관된 자식 엔티티를 모두 추출하여 사용한다.

Sample Source

   qlBuf.append("SELECT user ");
   // JOIN FETCH 를 이용함.
   qlBuf.append("FROM User user join fetch user.roles role ");
   qlBuf.append("WHERE role.roleName = ?");
   Query query = em.createQuery(qlBuf.toString());
   query.setParameter(1, "Admin");
   List userList = query.getResultList();
 
   assertEquals("fail to match the size of user list.", 2, userList.size());
 
   for (int i = 0; i < userList.size(); i++) {
      User user = (User) userList.get(i);
 
      if (i == 0) {
         assertEquals("fail to match a user name.", "kim",user.getUserName());
         assertEquals("fail to match a user password.", "kim123",user.getPassword());
 
         Set roles = user.getRoles();
         assertEquals("fail to match the size of role list.", 1, roles.size());
      } else if (i == 1) {
         assertEquals("fail to match a user name.", "lee",user.getUserName());
         assertEquals("fail to match a user password.", "lee123",user.getPassword());
      ...

위의 예를 보면 [FROM User user join fetch user.roles role]에서 join fetch 라는 키워드를 쓴 것을 확인할 수 있다. 이것에 의한 생성된 SQL은 다음과 같다.

Generated SQL

SELECT ...
FROM USER user0_ INNER JOIN AUTHORITY roles1_ ON user0_.USER_ID=roles1_.USER_ID 
                 INNER JOIN ROLE role2_ ON roles1_.ROLE_ID=role2_.ROLE_ID 
WHERE role2_.ROLE_NAME=?

위에 SQL은 query.getResultList()에 의해서 실행되는 SQL로 JOIN에 관련된 ROLE정보를 모두 추출하는 것을 확인할 수 있다.

5.32 - Spring과 JPA 통합

Spring은 JPA 기반 DAO 클래스를 쉽게 구현하기 위해 JpaTemplate을 제공하지만, 직접 EntityManager 메서드를 사용하는 방식(plain JPA)도 지원한다. JPA를 사용하기 위해서는 persistence.xml과 Spring ApplicationContext 설정이 필요하다.

Spring Integration

Spring에서는 JPA 기반에서 DAO 클래스를 쉽게 구현할 수 있도록 하기 위해 JdbcTemplate,HibernateTemplate등처럼 JpaTemplate을 제공한다. 하지만 JPA에 있어서는 Entity Manager의 Method를 직접 이용하는 것(plain JPA)에 대한 것도 가이드한다. 이에 두가지 방법에 대한 설정 및 사용방법에 대해서 설명하고자 한다. Spring JPA

기본 설정

Spring 기반하에서 JPA를 쓰고자 할 때 필요한 설정은 persistence.xml과 ApplicationContext 파일 설정이 필요하다.

persistence.xml 설정

<persistence-unit name="HSQLMUnit" transaction-type="RESOURCE_LOCAL"> 
   // 구현체는 Hibernate	
   <provider>org.hibernate.ejb.HibernatePersistence</provider>
 
   // Entity Class List
   <class>egovframework.sample.model.bidirection.User</class>
   <class>egovframework.sample.model.bidirection.Role</class>
   <class>egovframework.sample.model.bidirection.Department</class>
   <exclude-unlisted-classes/>
 
   <properties>
      // DBMS별 다른 설정 여기는 HSQL 설정.
      <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/>
      <property name="hibernate.show_sql" value="true"/>
      <property name="hibernate.format_sql" value="true"/>
      <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
   </properties>
</persistence-unit>

위에서 Entity Class List와 그에 따르는 <exclude-unlisted-classes/>는 프로젝트내에 있는 엔티티 클래스중에 리스트하는 것만을 엔티티로 인식하도록 설정하는 것이고 dialect설정은 DBMS별 별도 설정이다. 위의 예에서는 HSQL 설정.

Application Context 설정

   // 1.Transation Manager 설정
   <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
      <property name="entityManagerFactory" ref="entityManagerFactory"/>
   </bean>	
 
   // 2.Entity Manager Factory 설정
   <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
      <property name="persistenceUnitName" value="HSQLMUnit"/>
      <property name="persistenceXmlLocation" value="classpath:META-INF/persistHSQLMemDB.xml"/>
      <property name="dataSource" ref="dataSource"/>
   </bean>
 
    // 3.DataSource 설정
   <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
      <property name="driverClassName" value="net.sf.log4jdbc.DriverSpy"/>
      <property name="url" value="jdbc:log4jdbc:hsqldb:mem:testdb"/>
      <property name="username" value="sa"/>
      <property name="password" value=""/>
      <property name="defaultAutoCommit" value="false"/>
   </bean>	
 
   // 4.JPA Annotation 사용 설정
   <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>	
 
   // 5.Annotation 사용 설정
   <context:component-scan base-package="egovframework"/>
 
   // 6.Annotation 기반의 Transaction 활성화 설정 
   <tx:annotation-driven />

위의 예를 살펴보면 1.Transation Manager 설정, 2.Entity Manager Factory 설정, 3.DataSource 설정, 4.JPA Annotation 사용 설정, 5.Annotation 사용 설정, 6.Annotation 기반의 Transaction 활성화 설정 으로 구분되어 설정되어 있고 1~4까지가 JPA를 위한 설정이다. 각자 쓰고자 할때 변경이 필요한 부분은 2,3,5 내역으로 2번 항목은 persistence.xml 파일 위치와 persistenceUnitName 설정, 3번 항목은 DBMS연결을 위한 DataSource 설정, 5번 항목은 package 설정이다.

JpaTemplate 이용

Spring에서 정의한 JpaDaoSupport를 상속받아 getJpaTemplate()를 통해서 Entity Method 등을 호출 작업할 수 있다.

DAO 클래스 Source

public class UserDAO extends JpaDaoSupport {
   // Application Context 에서 설정한 Entity Manager Factory 명을 지정하여 부모의 EntityManagerFactory를 설정한다.
   @Resource(name="entityManagerFactory")
   public void setEMF(EntityManagerFactory entityManagerFactory) {
      super.setEntityManagerFactory(entityManagerFactory);
   }
 
   // getTemplate()에 의한 입력	
   public void createUser(User user) throws Exception {
      this.getJpaTemplate().persist(user);
   }
 
   // getTemplate()에 의한 조회	
   public User findUser(String userId) throws Exception {
      return (User) this.getJpaTemplate().find(User.class, userId);
   }
 
   // getTemplate()에 의한 query .. find method 지원됨	
   public List findUserListAll() throws Exception {
      return this.getJpaTemplate().find("FROM User user ORDER BY user.userName");
   }
 
   // getTemplate()에 의한 삭제	
   public void removeUser(User user) throws Exception {
      this.getJpaTemplate().remove(this.getJpaTemplate().getReference(User.class, user.getUserId()));
   }
 
   // getTemplate()에 의한 수정
   public void updateUser(User user) throws Exception {
      this.getJpaTemplate().merge(user);
   }
}

위의 예를 보면 JpaDaoSupport 를 상속받아서 this.getJpaTemplate().method()를 통해서 기능을 구현하였다.

Entity 클래스 Source

@Entity
public class User implements Serializable {
 
   private static final long serialVersionUID = -8077677670915867738L;
 
   @Id
   @Column(name = "USER_ID", length=10)
   private String userId;
 
   @Column(name = "USER_NAME", length=20)
   private String userName;
 
   @Column(length=20)
   private String password;
 
   ...
}

위의 예제는 DAO 클래스에서 쓰인 User Entity Class 소스의 일부이다.

Plain JPA 이용

JPA에서 정의한 Entity Manager의 Entity Method를 호출 작업할 수 있다. Entity Manager를 통해 작업함으로써 Spring 환경하에서 Spring에 대한 의존성을 최소화 할 수 있다.

DAO 클래스 Source

public class RoleDAO {
   // Application Context 설정의 4.JPA Annotation 사용 설정에 의해서 정의가능한 것으로 Annotation기반으로 Entity Manager를 지정한다.	
   @PersistenceContext
   private EntityManager em;
 
   // EntityManager를 통한 입력	
   public void createRole(Role role) throws Exception {
      em.persist(role);
   }
 
   // EntityManager를 통한 조회
   public Role findRole(String roleId) throws Exception {
      return (Role) em.find(Role.class, roleId);
   }
 
   // EntityManager를 통한 Query
   public List findRoleListAll() throws Exception {
      Query query = em.createQuery("FROM Role role ORDER BY role.roleName");
      return  query.getResultList();		
   }
 
   // EntityManager를 통한 삭제
   public void removeRole(Role role) throws Exception {
      em.remove(em.getReference(Role.class, role.getRoleId()));
   }
 
   // EntityManager를 통한 수정
   public void updateRole(Role role) throws Exception {
      em.merge(role);
   }
}

위의 예를 보면 Entity Manager의 메소드를 통해서 기능을 구현하였다

Entity 클래스 Source

@Entity
public class Role implements Serializable {
 
   private static final long serialVersionUID = 1042037005623082102L;
 
   @Id
   @Column(name = "ROLE_ID", length=10)
   private String roleId;
 
   @Column(name = "ROLE_NAME", length=20)
   private String roleName;
 
   @Column(name = "DESC" , length=50)	
   private String desc;	
   ...
}

위의 예제는 DAO 클래스에서 쓰인 Role Entity Class 소스의 일부이다.

5.33 - JPA Configuration

JPA는 persistence.xml 파일을 기반으로 동작하며, 이 파일은 실행 속성을 포함하고 여러 개의 persistence-unit을 정의할 수 있다. persistence.xml은 JPA 설정의 핵심 요소로, 상위에 태그를 포함하고 있다.

JPA Configuration

JPA는 실행 속성을 포함하고 있는 persistence.xml을 기반으로 하여 동작하도록 구성되어 있다. persistence.xml 파일의 주요 구성 요소와 속성 정의 방법에 대해 살펴보기로 한다. 먼저, persistence.xml 파일은 가장 상위에 <persistence> 태그를 포함하고 있으며 <persistence> 태그 내에 여러개의 <persistence-unit>를 포함할 수 있다.

Persistence Unit

Persistence Unit에 포함하고 있는 주요한 엔티티들은 다음과 같다.

element 명설 명
providerEntity Manager를 지원하는 Provider 클래스
mapping-file매핑정보 파일
classEntity 클래스 리스트, @Entity, @Embeddable or @MappedSuperclass 를 포함하고 있는 클래스
exclude-unlisted-classesclass 에서 정의하지 않은 것은 제외
propertiesJPA 구현체 프로퍼티 리스트

상세한 정보는 스키마 참조 아래는 위의 항목을 포함하고 있는 설정파일 예입니다.

   <persistence-unit name="HSQLMUnit" transaction-type="RESOURCE_LOCAL">
 
      <provider>org.hibernate.ejb.HibernatePersistence</provider>
      <class>egovframework.sample.model.bidirection.User</class>
      <exclude-unlisted-classes/>
      <properties>
         <property name="hibernate.connection.driver_class" value="net.sf.log4jdbc.DriverSpy"/>
      ...

Hibernate Properties

Properties 아래에 정의되는 Vendor별 설정 정보중에 Hibernate의 설정정보에 대해서 설명한다. 좀더 자세한 사항은 Hibernate사이트를 참조한다.

DataSource 속성

아래의 속성들을 통해 Hibernate는 특정 DB에 접근하여 데이터 액세스 처리가 가능하다.

속 성 명설 명
hibernate.connection.driver_class접근 대상이 되는 DB의 Driver 클래스명을 정의하기 위한 속성
hibernate.connection.url접근 대상이 되는 DB의 URL을 정의하기 위한 속성
hibernate.connection.usernameDB에 접근할 때 사용할 사용자명을 정의하기 위한 속성
hibernate.connection.passwordDB에 접근할 때 사용할 패스워드를 정의하기 위한 속성

다음은 위에서 언급한 속성들을 포함하고 있는 persistence.xml 파일의 일부이다

   <property name="hibernate.connection.driver_class" value="net.sf.log4jdbc.DriverSpy"/>
   <property name="hibernate.connection.url" value="jdbc:log4jdbc:hsqldb:mem:testdb"/>
   <property name="hibernate.connection.username" value="sa"/>

Generated SQL 속성

아래의 속성들을 통해 Hibernate는 특정 DB에 접근하여 데이터 액세스 처리가 가능하다.

속 성 명설 명
hibernate.dialectHibernate 기반 개발시 DB에 특화된 SQL을 구성하지 않더라도 DB에 따라 알맞은 SQL을 생성할 수 있다. 이를 위해서 Dialect 클래스를 사용한다. hibernate.dialect는 Dialect 클래스명을 정의하기 위한 속성
hibernate.default_schemaHibernate에서 SQL을 생성할 때 특정 테이블에 대해 별도 정의된 Schema가 없는 경우 적용할 DB Schema 명을 정의하기 위한 속성
hibernate.default_catalogHibernate에서 SQL을 생성할 때 특정 테이블에 대해 별도 정의된 Catalog가 없는 경우 적용할 DB Catalog 명을 정의하기 위한 속성

다음은 위에서 언급한 속성들을 포함하고 있는 persistence.xml 파일의 일부이다

   <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/>

다음은 Hibernate에서 제공하는 주요 Dialect 클래스 목록이다

DB 종류Dialect 클래스
Oracle 10gorg.hibernate.dialect.Oracle10gDialect
Oracle 9i/10iorg.hibernate.dialect.Oracle9iDialect
Oracle (모든 버전)org.hibernate.dialect.OracleDialect
MySQL 5.xorg.hibernate.dialect.MySQL5Dialect
MySQL 4.x, 3.xorg.hibernate.dialect.MySQLDialect
DB2org.hibernate.dialect.DB2Dialect
Sybase 11.9.2org.hibernate.dialect.Sybase11Dialect
Sybase Anywhereorg.hibernate.dialect.SybaseAnywhereDialect

Cache 속성

아래의 속성들을 통해 Hibernate는 Cache 기능을 지원한다.

속 성 명설 명
hibernate.cache.provider_classCache 기능을 구현하고 있는 구현체의 클래스명을 정의하기 위한 속성
hibernate.cache.use_second_level_cache2nd Level Cache를 적용할지 여부를 정의하기 위한 속성 (true/false)
hibernate.cache.use_query_cacheHibernate Query를 Caching할지 여부를 정의하기 위한 속성 (true/false)

다음은 위에서 언급한 속성들을 포함하고 있는 persistence.xml 파일의 일부이다

   <property name="hibernate.cache.use_second_level_cache" value="true"/> 
   <property name="hibernate.cache.use_query_cache" value="true"/> 
   <property name="hibernate.cache.provider_class" value="org.hibernate.cache.EhCacheProvider"/>

Logging 속성

아래의 속성들을 통해 Hibernate는 좀더 자세한 Logging 기능을 지원한다.

속 성 명설 명
hibernate.show_sqlHibernate을 통해 생성된 SQL을 콘솔에 남길 것인지 여부를 정의하는 속성 (true/false)
hibernate.format_sqlhibernate.show_sql=true인 경우 해당 SQL문의 포맷을 정돈하여 콘솔에 남길 것인지 여부를 정의하는 속성 (true/false)
hibernate.use_sql_commentsHibernate을 통해 생성된 SQL을 콘솔에 남길 때 Comments도 같이 남길 것인지 여부를 정의하는 속성 (true/false)

다음은 위에서 언급한 속성들을 포함하고 있는 persistence.xml 파일의 일부이다

   <property name="hibernate.show_sql" value="true"/>
   <property name="hibernate.format_sql" value="true"/>

기타 속성

속 성 명설 명
hibernate.hbm2ddl.autoDDL을 자동으로 검증,생성 또는 수정할지 여부를 정의하기 위한 속성 (validate/ update/ create/ create-drop)
hibernate.jdbc.batch_sizeHibernate는 일반적으로 실행해야 할 SQL들에 대해 일괄적으로 batch 처리를 수행하는데 이 때 batch로 처리할 SQL의 개수를 정의하기 위한 속성

다음은 위에서 언급한 속성들을 포함하고 있는 persistence.xml 파일의 일부이다

   <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
   <property name="hibernate.jdbc.batch_size" value="10" />

5.34 - Transaction 서비스

Spring 트랜잭션 서비스는 DataSource, JTA, JPA 트랜잭션 서비스를 지원하며, 트랜잭션 관리는 선언적 방식(Declarative)과 프로그래밍 방식(Programmatic)으로 수행할 수 있다. DataSource 트랜잭션 서비스는 로컬 트랜잭션 관리를 위한 설정 및 사용법을 제공한다.

Transaction 서비스

개요

트랜잭션 서비스는 Spring 트랜잭션 서비스를 채택하여 가이드한다. 트랜잭션 서비스에는 여러가지가 있지만 여기서는 DataSource Transaction Service, JTA Transaction Service, JPA Transaction Service에 대해서 설명하고 트랜잭션 활용에 대해서는 설정 및 Annotation을 통해서 활용할 수 있는 Declaration Transaction Management와 프로그램에서 직접 API를 호출하여 쓸 수 있도록 하는 Programmatic Transaction Management 두가지에 대해서 설명한다.

설명

DataSource Transaction Service

DataSource를 사용하여 Local Transaction을 관리할 수 있다. 아래에서 예를 들어서 설정 방법과 사용법을 설명한다.

Configuration

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="dbc:mysql://db2:1621/rte"/>
    <property name="username" value="rte"/>
    <property name="password" value="xxx"/>
    <property name="defaultAutoCommit" value="false"/>
</bean>
PROPERTIES설 명
driverClassNamejdbc driver
urldb url
username사용자명
password패스워드
defaultAutoCommit자동commit 설정

위의 설정을 보면 transactionManager의 property로 dataSource를 지정하고 그에 필요한 driver정보,Url정보등을 지정한 것을 확인 할 수 있다. 설정한 dataSource 기반하에서 트랜잭션 서비스를 제공한다. 사이트 환경에 맞추어 driverClassName,url,username,password는 변경해서 적용한다.

Sample Source

    @Resource(name="transactionManager")
    PlatformTransactionManager transactionManager;
    ...
    TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);

위의 예와 같이 transactionManager을 활용할 수 있다.

JTA Transaction Service

자바 트랜잭션 API(Java Transaction API, JTA)는 XA 리소스(예, 데이터베이스) 간의 분산 트랜잭션을 처리하는 자바 API이다.

Java에서 제공되는 대부분의 API와 마찬가지로, JTA는 실제 구현은 다르지만 어플리케이션이 공통적으로 사용할 수 있는 하나의 인터페이스를 제공한다. 이 말은 트랜잭션 처리가 필요한 어플리케이션이 (API의 사용 방식 그대로만 사용한다면) 특정 벤더의 트랜잭션 매니저에 의존할 필요가 없음을 의미한다.

그에따라, JTA는 일반적으로 단일 데이터베이스 또는 여러 개의 데이터베이스를 이용할 경우 트랜잭션을 제어하기 위한 목적으로 사용된다.

JTA를 이용하여 Global Transation관리를 할 수 있도록 지원하는 것으로 아래에서 예를 들어서 설정 방법을 설명한다. 사용법은 DataSource Transaction Service와 동일하다.

Configuration

    <tx:jta-transaction-manager/>
    <jee:jndi-lookup id="dataSource" jndi-name="dbmsXADS" resource-ref="true">
      <jee:environment>
         java.naming.factory.initial=weblogic.jndi.WLInitialContextFactory
         java.naming.provider.url=t3://was:7002
      </jee:environment>
    </jee:jndi-lookup>

위의 설정예에서 jndi-name 과 java.naming.factory.initial,java.naming.provider.url은 사이트 환경에 맞추어 변경해야 한다. DataSource Transaction Service와는 달리 transationManager에 대해서 따로 bean 정의하지 않아도 된다.

JPA Transaction Service

JPA Transaction 서비스는 JPA EntityManagerFactory를 이용하여 트랜잭션을 관리한다. JpaTransactionManager는 EntityManagerFactory에 의존성을 가지고 있으므로 반드시 EntityManagerFactory 설정과 함께 정의되어야 한다. 아래에서 예를 들어서 설정 방법을 설명한다. 사용법은 DataSource Transaction Service와 동일하다.

Configuration

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceUnitName" value="OraUnit"/>
<property name="persistenceXmlLocation" value="classpath:META-INF/persistence.xml"/>
<property name="dataSource" ref="dataSource"/>
</bean>

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="dbc:mysql://db2:1621/rte"/>
<property name="username" value="rte"/>
<property name="password" value="xxx"/>
<property name="defaultAutoCommit" value="false"/>
</bean>
PROPERTIES설 명
driverClassNamejdbc driver
urldb url
username사용자명
password패스워드
defaultAutoCommit자동commit 설정
persistenceUnitNamepersistenceUnitName
persistenceXmlLocationXML 위치
dataSource데이타소스

위의 설정을 보면 transactionManager의 property로 entiyManagerFactory로 지정하고 entityManagerFactory의 property로 dataSource를 지정하고 그에 필요한 driver정보,Url정보등을 지정한 것을 확인 할 수 있다. 설정한 dataSource 기반하에서 트랜잭션 서비스를 제공한다. 사이트 환경에 맞추어 driverClassName,url,username,password는 변경해서 적용한다. 또한 persistenceUnitName과 persistenceXmlLocation 정보를 지정하는 것을 알수 있다.

참고자료

5.35 - 선언적 Transaction 관리

Spring에서는 트랜잭션을 코드에서 직접 처리하지 않고, 애노테이션이나 XML 정의를 통해 선언적으로 관리할 수 있다. 애노테이션 기반 트랜잭션 관리는 설정과 사용법을 통해 간편하게 트랜잭션을 처리할 수 있다.

Declarative Transaction Management

개요

코드에서 직접적으로 Transaction 처리하지 않고, 선언적으로 Transaction을 관리할 수 있는데 Annotation을 이용한 Transaction 관리, XML 정의를 이용한 Transaction 관리를 지원한다.

설명

Annotation Transaction Management

Annotation 설정을 이용해서 Transaction을 관리할 수 있는데 아래에서 예를 들어서 설정 방법과 사용법을 설명한다.

Configuration

<tx:annotation-driven transaction-manager="transactionManager" />

설정 XML에 위의 <tx:annotation-driven..>을 기재하면 설정된다. transactionManager는 TransactionManager 설정 참조

Sample Source

@Transactional
public void removeRole(Role role) throws Exception {
    this.roleDAO.removeRole(role);
}

위의 예를 보면 @Transactional을 트랜잭션 처리하고자 하는 메소드위에 기재하여 트랜잭션 관리를 할 수 있다. @Transactional에 속성을 정의하여 쓸 수 있는데 속성 목록은 아래와 같다.

속 성설 명사 용 예
isolationTransaction의 isolation Level 정의하는 요소. 별도로 정의하지 않으면 DB의 Isolation Level을 따름.@Transactional(isolation=Isolation.DEFAULT)
noRollbackFor정의된 Exception 목록에 대해서는 rollback을 수행하지 않음.@Transactional(noRollbackFor=NoRoleBackTx.class)
noRollbackForClassNameClass 객체가 아닌 문자열을 이용하여 rollback을 수행하지 않아야 할 Exception 목록 정의@Transactional(noRollbackForClassName=“NoRoleBackTx”)
propagationTransaction의 propagation 유형을 정의하기 위한 요소@Transactional(propagation=Propagation.REQUIRED)
readOnly해당 Transaction을 읽기 전용 모드로 처리 (Default = false)@Transactional(readOnly = true)
rollbackFor정의된 Exception 목록에 대해서는 rollback 수행@Transactional(rollbackFor=RoleBackTx.class)
rollbackForClassNameClass 객체가 아닌 문자열을 이용하여 rollback을 수행해야 할 Exception 목록 정의@Transactional(rollbackForClassName=“RoleBackTx”)
timeout지정한 시간 내에 해당 메소드 수행이 완료되지 않은 경우 rollback 수행. -1일 경우 no timeout (Default = -1)@Transactional(timeout=10)

Configurational Transaction Management

XML 정의 설정을 이용해서 Transaction을 관리할 수 있는데 아래에서 예를 들어서 설정 방법과 사용법을 설명한다.

Configuration

<aop:config>
    <aop:pointcut id="requiredTx" expression="execution(* egovframework.sample..impl.*Impl.*(..))"/>
    <aop:advisor advice-ref="txAdvice" pointcut-ref="requiredTx" />
</aop:config>

<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
    <tx:method name="find*" read-only="true"/>
    <tx:method name="createNoRBRole" no-rollback-for="NoRoleBackTx"/>
    <tx:method name="createRBRole" rollback-for="RoleBackTx"/>
    <tx:method name="create*"/>
</tx:attributes>
</tx:advice>

위의 설정을 보면 aop:pointcut를 이용하여 실행되어 Catch해야 하는 Method를 지정하고 tx:advice를 통해서 각각에 대한 룰을 정의하고 있다. 이렇게 정의하면 프로그램내에서는 별도의 트랜잭션 관련한 사항에 대해서 기술하지 않아도 트랜잭션관리가 된다. 위 샘플 XML에서와 같이 Transaction 관리를 위해 tx:advice 하위 태그인 tx:method에는 다음과 같은 상세 속성 정보를 부여할 수 있다. 관련 속성 정보는 아래와 같다.

속 성설 명사 용 예
name메소드명 기술. 와일드카드 사용 가능함name=“find*”
isolationTransaction의 isolation Level 정의하는 요소. 별도로 정의하지 않으면 DB의 Isolation Level을 따름.isolation=“DEFAULT”
no-rollback-for정의된 Exception 목록에 대해서는 rollback을 수행하지 않음.no-rollback-for=“NoRoleBackTx”
propagationTransaction의 propagation 유형을 정의하기 위한 요소propagation=“REQUIRED”
read-only해당 Transaction을 읽기 전용 모드로 처리 (Default = false)read-only=“true”
rollback-for정의된 Exception 목록에 대해서는 rollback 수행rollback-for=“RoleBackTx”
timeout지정한 시간 내에 해당 메소드 수행이 완료되지 않은 경우 rollback 수행. -1일 경우 no timeout (Default = -1)timeout=“10”

관련 속성별 가능 값 정보는 스키마 참조.

Propagation Behavior, Isolation Level

위에서 설명한 두가지 Transaction Management에 공통적으로 사용되는 항목은 Propagation 과 Isolation Level에 대한 설명을 하고자 한다.

Propagation Behavior

트랜잭션의 전파 규칙을 설정하기 위해 사용한다.

속 성 명설 명
PROPAGATION_MADATORY반드시 Transaction 내에서 메소드가 실행되어야 한다. 없으면 예외발생
PROPAGATION_NESTEDTransaction에 있는 경우, 기존 Transaction 내의 nested transaction 형태로 메소드를 실행하고, nested transaction 자체적으로 commit, rollback이 가능하다. Transaction이 없는 경우, PROPAGATION_REQUIRED 속성으로 행동한다. nested transaction 형태로 실행될 때는 수행되는 변경사항이 커밋이 되기 전에는 기존 Transaction에서 보이지 않는다.
PROPAGATION_NEVERManatory와 반대로 Transaction 없이 실행되어야 하며 Transaction이 있으면 예외를 발생시킨다.
PROPAGATION_NOT_SUPPORTEDTransaction 없이 메소드를 실행하며,기존의 Transaction이 있는 경우에는 이 Transaction을 호출된 메소드가 끝날 때까지 잠시 보류한다
PROPAGATION_REQUIRED기존 Transaction이 있는 경우에는 기존 Transaction 내에서 실행하고, 기존 Transaction이 없는 경우에는 새로운 Transaction을 생성한다.
PROPAGATION_REQUIRED_NEW호출되는 메소드는 자신 만의 Transaction을 가지고 실행하고, 기존의 Transaction들은 보류된다
PROPAGATION_SUPPORTS새로운 Transaction을 필요로 하지는 않지만, 기존의 Transaction이 있는 경우에는 Transaction 내에서 메소드를 실행한다.

Isolation Level

Transaction에서 일관성이 없는 데이터를 허용하도록 하는 수준이며, 여러 Transaction들이 다른 Transaction의 방해로부터 보호되는 정도를 나타낸다. 좀더 자세한 설명은 여기 참고.

속 성 명설 명
ISOLATION_DEFAULT개별적인 PlatformTransactionManager를 위한 격리 레벨
ISOLATION_READ_COMMITTED이 격리수준을 사용하는 메소드는 commit 되지 않은 데이터를 읽을 수 없다. 쓰기 락은 다른 Transaction에 의해 이미 변경된 데이터는 얻을수 없다. 따라서 조회 중인 commit 되지 않은 데이터는 불가능하다. 대개의 데이터베이스에서의 디폴트로 지원하는 격리 수준이다.
ISOLATION_READ_UNCOMMITTED가장 낮은 Transaction 수준이다. 이 격리수준을 사용하는 메소드는 commit 되지 않은 데이터를 읽을 수 있다. 그러나 이 격리수준은 새로운 레코드가 추가되었는지 알수 없다.
ISOLATION_REPEATABLE_READISOLATION_READ_COMMITED 보다는 다소 조금 더 엄격한 격리 수준이다. 이 격리 수준은 다른 Transaction이 새로운 데이터를 입력했다면, 새롭게 입력된 데이터를 조회할 수 있다는 것을 의미한다.
ISOLATION_SERIALIZABLE가장 높은 격리수준이다. 모든 Transaction(조회를 포함하여)은 각 라인이 실행될 때마다 기다려야 하기 때문에 매우 느리다. 이 격리수준을 사용하는 메소드는 데이터 상에 배타적 쓰기를 락을 얻음으로써 Transaction이 종료될 때까지 조회, 수정, 입력 데이터로부터 다른 Transaction의 처리를 막는다. 가장 많은 비용이 들지만 신뢰할만한 격리 수준을 제공하는 것이 가능하다.

참고자료

5.36 - 프로그래밍 방식의 Transaction 관리

프로그래밍 방식으로 트랜잭션을 관리할 때는 TransactionTemplate을 사용하거나 Transaction Manager를 사용하는 방법이 있다. TransactionTemplate은 콜백 메소드를 정의해 트랜잭션을 처리하는 방법을 제공한다.

Programmatic Transaction Management

개요

프로그램에서 직접 트랜잭션을 관리하고자 할 때 사용할 수 있는 방법에 대해서 설명하고자 한다. TransactionTemplate를 사용하는 방법과 Trnasaction Manager를 사용하는 방법 두가지가 있다.

설명

TransactionTemplate를 사용하는 방법

TransactionTemplate를 정의하고 callback 메소드 정의를 이용하여 Transaction 관리를 할 수 있도록 하는 방법이다.

Configuration

<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
    <property name="transactionManager" ref="transactionManager"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>
PROPERTIES설 명
transactionManager트랜잭션매니저
dataSource데이타소스

위의 설정에서 transactionTemplate를 정의하고 property로 transactionManager을 정의한다. Templeate를 이용한 샘플은 아래와 같다.

Sample Source<

@Test
public void testInsertCommit() throws Exception {
   transactionTemplate.execute(new TransactionCallbackWithoutResult() {
      public void doInTransactionWithoutResult(TransactionStatus status) {
         try {
            Role role = new Role();
            role.setRoleId("ROLE-001");
            role.setRoleName("ROLE-001");
            role.setRoleDesc(new Integer(1000));
            roleService.createRole(role);
         } catch (Exception e) {
            status.setRollbackOnly();
         }
      }
   });		
   Role retRole = roleService.findRole("ROLE-001");		
   assertEquals("roleName Compare OK",retRole.getRoleName(),"ROLE-001");
}

위의 예에서 transactionTemplate.execute에 TransactionCallbackWithoutResult를 정의하여 Transaction 관리를 하는 것을 확인할 수 있다.

사용 방법

Transaction Context에 의해 호출될 callback 메소드를 정의하고 이 메소드 내에 비즈니스 로직을 구현해주면 된다. 아래는 사용하는 방법의 예이다

    this.transactionTemplate.execute(new TransactionCallbackWithoutResult() {                
        public void doInTransactionWithoutResult(TransactionStatus status) {                    
            //... biz. logic ...       
        }
    });
 
    this.transactionTemplate.execute(new TransactionCallback() {                
        public Object doInTransaction(TransactionStatus status) {                    
            //... biz. logic ...       
        }
    });

callback 메소드 doInTransactionWithoutResult()는 Result 값이 없을 경우에 사용하고, Result 값이 존재하는 경우에는 doInTransaction()으로 사용하도록 한다. 또한, callback 메소드 내에서 입력 인자인 TransactionStatus 객체의 setRollbackOnly() 메소드를 호출함으로써 해당 Transaction을 rollback할 수 있다.

Trnasaction Manager를 사용하는 방법

Transaction Manager를 직접 이용하는 방법이다.

Configuration

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
   <property name="dataSource" ref="dataSource"/>
</bean>
PROPERTIES설 명
dataSource데이타소스

위의 설정에서 transactionManager을 정의한다.

Sample Source

@Test
public void testInsertRollback() throws Exception {
 
    int prevCommitCount = roleService.getCommitCount();
    int prevRollbackCount = roleService.getRollbackCount();
 
    DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
    txDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
    TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
 
    try {
        Role role = new Role();
        role.setRoleId(Thread.currentThread().getName() + "-roleId");
        role.setRoleName(Thread.currentThread().getName() + "-roleName");
        role.setRoleDesc(new Integer(1000));
        roleService.createRole(role);
        roleService.createRole(role);
        transactionManager.commit(txStatus);		
    } 
    catch (Exception e) {
        transactionManager.rollback(txStatus);
    } finally {
        assertEquals(prevCommitCount, roleService.getCommitCount());
        assertEquals(prevRollbackCount + 2, roleService.getRollbackCount());
    }
}

Transaction 서비스를 직접 얻어온 후에 위와 같이 try~catch 구문 내에서 Transaction 서비스를 이용하여, 적절히 begin, commit, rollback을 수행한다. 이 때, TransactionDefinition와 TransactionStatus 객체를 적절히 이용하면 된다.

참고자료

5.37 - Spring Data - Reactive

Spring Data는 데이터베이스와의 상호작용을 단순화하고 다양한 데이터 저장소 기술을 지원하는 하위 프로젝트로, Reactive 프로그래밍과의 연동을 지원한다. 이를 통해 NoSQL 데이터베이스인 R2DBC, MongoDB, Cassandra, Redis와의 비동기적 데이터 처리 기능을 제공한다. 전자정부 표준프레임워크에서도 이와 관련된 라이브러리를 지원한다.

Spring Data - Reactive

개요

Spring Data는 스프링 프레임워크의 하위 프로젝트 중 하나로, 데이터 액세스를 단순화하고 보다 쉽게 관리할 수 있도록 지원하는 도구 모음이다. 주로 데이터베이스와의 상호 작용을 다루며, 다양한 데이터 저장소 및 데이터 액세스 기술을 지원한다.
여기서는 NoSQL 데이터베이스인 R2DBC, Spring Data MongoDB, Cassandra, Redis와 Spring Reactive 연동에 전자정부 표준프레임워크에서 지원하는 라이브러리에 대해 설명한다.
자세한 내용은 아래 페이지에서 확인할 수 있다.

5.38 - R2DBC

R2DBC는 비동기적 방식으로 관계형 데이터베이스와 상호작용하기 위한 자바 라이브러리로, Spring WebFlux와 함께 리액티브 애플리케이션을 구성할 수 있다. 데이터베이스에 액세스하려면 ConnectionFactory 객체를 생성하여 공통으로 사용하며, 데이터베이스 종류에 따라 다른 구현체를 사용한다.

R2DBC

개요

R2DBC(Relational Reactive Database Connectivity)는 Reactive 프로그래밍 모델을 기반으로 하는 비동기적인 방식으로 관계형 데이터베이스와 상호 작용하기 위한 자바 라이브러리로 Spring WebFlux와 함께 사용하여 비동기 논블로킹 방식의 애플리케이션을 구성할 수 있다. 이를 통해 리액티브 애플리케이션 스택에서 관계형 데이터 액세스 기술을 사용하는 Spring 기반 애플리케이션을 더 쉽게 빌드할 수 있다.

설명

데이터베이스 연동

R2DBC를 사용하여 데이터베이스에 액세스하기 위해 가장 먼저 해야 할 일은 JDBC의 DataSource와 비슷한 역할을 하는 ConnectionFactory 객체를 만드는 것이다.
ConnectionFactory를 생성하는 가장 간단한 방법은 ConnectionFactories 클래스를 사용하는 것인데 이 클래스에는 ConnectionFactoryOptions 객체를 받아 ConnectionFactory를 반환하는 정적 메서드가 있다.
ConnectionFactory의 인스턴스는 하나만 필요하며, 데이터베이스 종류 및 데이터베이스 드라이버에 따라 구현체가 다를 수 있으므로 공통으로 사용할 수 있게 실행환경에 구성하고 애플리케이션 구성에서 필요할 때마다 주입을 통해 사용할 수 있도록 제공한다.

실행환경 라이브러리

package org.egovframe.rte.psl.reactive.r2dbc.connect;
 
public class EgovR2dbcConnectionFactory {
    public ConnectionFactory connectionFactory() {
        return ConnectionFactories.get(this.r2dbcUrl);
    }
}

적용 예제

package egovframework.webflux.config;
 
import org.egovframe.rte.psl.reactive.r2dbc.connect.EgovR2dbcConnectionFactory;
......
 
@Configuration
public class EgovR2dbcConfig {
 
    @Bean(name="connectionFactory")
    public ConnectionFactory connectionFactory() {
        EgovR2dbcConnectionFactory egovR2dbcConnectionFactory = new EgovR2dbcConnectionFactory(this.r2dbcUrl);
        return egovR2dbcConnectionFactory.connectionFactory();
    }
}

Repository 구성

R2DBC는 스프링 생태계와 통합되어 있어 스프링 기반 애플리케이션에서 쉽게 사용할 수 있으며, 스프링 데이터 R2DBC 프로젝트를 통해 스프링의 강력한 기능과 R2DBC의 비동기 데이터베이스 액세스를 결합할 수 있다.
R2dbcEntityTemplate은 객체와 관계형 데이터베이스 간의 매핑을 지원하므로 SQL 쿼리를 직접 작성하지 않고도 객체를 데이터베이스 테이블에 매핑할 수 있으며, CRUD 작업을 단순화하여 개발자는 복잡한 SQL 쿼리를 작성하는 대신 객체 지향적인 방식으로 데이터베이스와 상호 작용할 수 있어 코드 가독성이 향상되고 유지 보수가 용이해진다.

실행환경 라이브러리

@Repository 클래스에 EgovR2dbcRepository 클래스를 extends 하여 insertData, updateData, deleteData, selectAllData 메소드를 활용한다.

package org.egovframe.rte.psl.reactive.r2dbc.repository;
 
public class EgovR2dbcRepository<T> extends R2dbcEntityTemplate {
 
    public EgovR2dbcRepository(ConnectionFactory connectionFactory) {
        super(connectionFactory);
    }
 
    public Flux<T> selectAllData(Query query, Class<T> entityClass) {
        return select(query, entityClass);
    }
 
    public Mono<T> selectOneData(Query query, Class<T> entityClass) {
        return selectOne(query, entityClass);
    }
 
    public Mono<Long> countData(Query query, Class<T> entityClass) {
        return count(query, entityClass);
    }
 
    public Mono<T> insertData(T entity) {
        return insert(entity);
    }
 
    public Mono<T> updateData(T entity) {
        return update(entity);
    }
 
    public Mono<T> deleteData(T entity) {
        return delete(entity);
    }
}

참고자료

5.39 - MongoDB

Spring Data MongoDB는 MongoDB 문서형 데이터 저장소와의 연동을 위한 고수준 추상화 템플릿을 제공하며, Spring JDBC 지원 방식과 유사하다. SimpleReactiveMongoDatabaseFactory 클래스를 사용해 MongoDB 연결을 관리하고, 데이터 액세스 작업에 집중할 수 있도록 지원한다.

MongoDB

개요

Spring Data MongoDB 프로젝트는 MongoDB 문서 스타일 데이터 저장소를 사용하는 솔루션 개발에 Spring의 핵심 개념을 적용하여 문서를 저장하고 쿼리하기 위한 높은 수준의 추상화 템플릿을 제공한다. Spring 프레임워크에서 제공하는 JDBC 지원과 유사하다는 것을 알 수 있다.

설명

데이터베이스 연동

Spring WebFlux에서 MongoDB 데이터베이스와 연결을 설정하고 관리하기 위해서는 ReactiveMongoDatabaseFactory 인터페이스의 구현클래스인 SimpleReactiveMongoDatabaseFactory 클래스를 사용하여, 연결 풀링이나 커넥션 관리 기능 등을 추상화하여 데이터 액세스 작업에 집중할 수 있게 한다.

실행환경 라이브러리

package org.egovframe.rte.psl.reactive.mongodb.connect;
 
public class EgovMongoDbConnectionFactory {
    public ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory() {
        ConnectionString connectionString = new ConnectionString(this.mongoDbUrl);
        MongoClientSettings mongoClientSettings = MongoClientSettings.builder()
                .applyConnectionString(connectionString).build();
        return new SimpleReactiveMongoDatabaseFactory(
            MongoClients.create(mongoClientSettings), this.mongoDbName);
    }
}

적용 예제

package egovframework.webflux.config;
 
import org.egovframe.rte.psl.reactive.mongodb.connect.EgovMongoDbConnectionFactory;
......
 
@Configuration
public class EgovMongodbConfig {
 
    @Bean(name="reactiveMongoDatabaseFactory")
    public ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory() {
        EgovMongoDbConnectionFactory egovMongoDbConnectionFactory = new EgovMongoDbConnectionFactory(this.mongoDBName, this.mongoDBUrl);
        return egovMongoDbConnectionFactory.reactiveMongoDatabaseFactory();
    }
 
    @Bean
    public ReactiveMongoTransactionManager transactionManager(@Qualifier("reactiveMongoDatabaseFactory") ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory) {
        return new ReactiveMongoTransactionManager(reactiveMongoDatabaseFactory);
    }
}

Repository 구성

ReactiveMongoTemplate 클래스를 이용하여 Repository를 구성하면 MongoDB와 상호 작용하는 동안 Reactive 프로그래밍 모델을 활용할수 있고, Async Non-Blocking 데이터 처리가 가능하여 애플리케이션이 데이터베이스 작업을 대기하지 않고 다른 작업을 수행할 수 있도록 하며, 성능 향상을 실현할 수 있다. 또한 높은 동시 요청을 처리하기 위해 최적화되어 있어 논블로킹 작업을 통해 많은 동시 연결을 처리할 수 있으므로, 대규모 및 고트래픽 애플리케이션에 적합하다. 그리고 Reactive 프로그래밍은 자원을 효율적으로 사용할 수 있도록 도와준다. MongoDB 작업이 블로킹되지 않으므로 스레드 풀 및 메모리 자원을 효율적으로 관리할 수 있으며 MongoDB 연결을 관리하고 에러 처리를 담당하여 연결 풀링, 재시도 메커니즘 및 예외 처리를 제공하여 안정성을 향상시킨다.

실행환경 라이브러리

@Repository 클래스에 EgovMongoDbRepository 클래스를 extends 하여 insertData, updateData, deleteData, selectAllData 메소드를 활용한다.

package org.egovframe.rte.psl.reactive.mongodb.repository;
 
public class EgovMongoDbRepository<T> extends ReactiveMongoTemplate {
    public EgovMongoDbRepository(ReactiveMongoDatabaseFactory reactiveMongoDatabaseFactory) {
        super(reactiveMongoDatabaseFactory);
    }
 
    public Flux<T> selectAllData(Query query, Class<T> entityClass) {
        return find(query, entityClass);
    }
 
    public Mono<T> selectOneData(Query query, Class<T> entityClass) {
        return findOne(query, entityClass);
    }
 
    public Mono<Long> countData(Query query, Class<T> entityClass) {
        return count(query, entityClass);
    }
 
    public Mono<T> insertData(T objectToSave) {
        return insert(objectToSave);
    }
 
    public Mono<T> updateData(Query query, Update update, Class<T> entityClass) {
        return findAndModify(query, update, entityClass);
    }
 
    public Mono<T> deleteData(Query query, Class<T> entityClass) {
        return findAndRemove(query, entityClass);
    }
}

참고자료

5.40 - Cassandra

Spring Data Cassandra는 Cassandra 데이터베이스와의 연동을 위한 고수준 추상화 템플릿을 제공하며, Spring의 JDBC 지원 방식과 유사하다. 비동기적인 상호 작용을 위해 DefaultBridgedReactiveSession 클래스를 사용하여 Cassandra 클러스터에 연결하고, 비동기 쿼리를 실행 및 관리할 수 있다.

Cassandra

개요

Cassandra를 위한 Spring 데이터 프로젝트는 핵심 Spring 개념을 Cassandra 컬럼형 데이터 저장소를 사용하는 솔루션 개발에 적용하여 문서를 저장하고 쿼리하기 위한 높은 수준의 추상화 템플릿을 제공한다. Spring 프레임워크에서 제공하는 JDBC 지원과 유사하다는 것을 알 수 있다.

설명

데이터베이스 연동

Spring Data Cassandra와 Spring WebFlux를 함께 사용하여 Cassandra 데이터베이스와의 비동기적인 상호 작용을 지원하기 위해 Spring Data Cassandra에서 제공하는 DefaultBridgedReactiveSession 클래스를 사용한다. 해당 클래스를 사용하여 Cassandra 클러스터에 대한 연결을 설정하고, 세션을 관리하며 비동기 쿼리를 실행하고 결과를 처리할 수 있다.

실행환경 라이브러리

package org.egovframe.rte.psl.reactive.cassandra.connect;
 
public class EgovCassandraConfiguration {
 
    public ReactiveSession reactiveSession() {
        return new DefaultBridgedReactiveSession(
            CqlSession.builder()
                .withLocalDatacenter(getDataCenterName())
                .withKeyspace(getKeyspaceName())
                .addContactPoint(InetSocketAddress.createUnresolved(getContactPoint(), getPort()))
                .withAuthCredentials(getUsername(), getPassword())
                .build()
        );
    }
}

적용 예제

package egovframework.webflux.config;
 
import org.egovframe.rte.psl.reactive.cassandra.connect.EgovCassandraConfiguration;
......
 
@Configuration
public class EgovCassandraConfig {
 
    @Bean(name="reactiveSession")
    public ReactiveSession reactiveSession() {
        EgovCassandraConfiguration egovCassandraConfiguration = new EgovCassandraConfiguration();
        egovCassandraConfiguration.setDataCenterName(this.dataCenterName);
        egovCassandraConfiguration.setKeyspaceName(this.keyspaceName);
        egovCassandraConfiguration.setContactPoint(this.contactPoints);
        egovCassandraConfiguration.setPort(this.port);
        egovCassandraConfiguration.setUsername(this.username);
        egovCassandraConfiguration.setPassword(this.password);
        return egovCassandraConfiguration.reactiveSession();
    }
}

Repository 구성

Spring WebFlux와 ReactiveCassandraTemplate을 사용하면 비동기, 논블로킹 처리와 높은 가용성을 통한 효율적인 Cassandra 데이터베이스 상호 작용을 달성할 수 있으며 확장성, 효율성, 그리고 반응형 스트리밍을 통해 데이터베이스 처리를 최적화할 수 있다. 또한 Flux 및 Mono와 같은 리액티브 타입을 사용하여 스트림 데이터를 다룰 수 있으며, 실시간 데이터 처리 및 웹 소켓과 같은 실시간 기능을 구현하기가 용이하다.

실행환경 라이브러리

@Repository 클래스에 EgovCassandraRepository 클래스를 extends 하여 insertData, updateData, deleteData, selectAllData 메소드를 활용한다.

package org.egovframe.rte.psl.reactive.cassandra.repository;
 
public class EgovCassandraRepository<T> extends ReactiveCassandraTemplate {
    public EgovCassandraRepository(ReactiveSession reactiveSession) {
        super(reactiveSession);
    }
 
    public Flux<T> selectAllData(Query query, Class<T> entityClass) {
        return select(query, entityClass);
    }
 
    public Mono<T> selectOneData(Query query, Class<T> entityClass) {
        return selectOne(query, entityClass);
    }
 
    public Mono<Long> countData(Query query, Class<T> entityClass) {
        return count(query, entityClass);
    }
 
    public Mono<T> insertData(T entity) {
        return insert(entity);
    }
 
    public Mono<T> updateData(T entity) {
        return update(entity);
    }
 
    public Mono<T> deleteData(T entity) {
        return delete(entity);
    }
}

참고자료

5.41 - Redis

Spring Data Redis는 키-값 데이터 저장소인 Redis와의 상호작용을 위해 높은 수준의 추상화 템플릿을 제공하며, Spring의 JDBC 지원 방식과 유사하다. LettuceConnectionFactory 클래스를 사용해 비동기적으로 Redis와 연결하고, 데이터베이스 세션 관리 및 쿼리 실행을 지원한다.

Redis

개요

Spring Data Redis 프로젝트는 키-값 스타일 데이터 저장소를 사용하여 솔루션 개발에 핵심 Spring 개념을 적용하여 메시지를 주고받기 위한 높은 수준의 추상화 템플릿을 제공한다. Spring 프레임워크의 JDBC 지원과 유사하다는 것을 알 수 있다.

설명

데이터베이스 연동

Spring Data Redis와 Spring WebFlux를 함께 사용하여 Redis 데이터베이스와의 비동기적인 상호 작용을 지원하기 위해 Spring Data Redis에서 제공하는 ReactiveRedisConnectionFactory 인터페이스의 구현클래스인 LettuceConnectionFactory 클래스를 사용한다. 해당 클래스를 사용하여 데이터베이스 연결을 설정하고, 세션을 관리하며 비동기 쿼리를 실행하고 결과를 처리할 수 있다.

실행환경 라이브러리

package org.egovframe.rte.psl.reactive.redis.connect;
 
public class EgovRedisConfiguration {
    public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory() {
        return new LettuceConnectionFactory(this.host, this.port);
    }
}

적용 예제

package egovframework.webflux.config;
 
@Configuration
public class EgovRedisConfig {
 
    @Bean(name="reactiveRedisConnectionFactory")
    public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory() {
        EgovRedisConfiguration egovRedisConfiguration = new EgovRedisConfiguration(this.host, this.port);
        return egovRedisConfiguration.reactiveRedisConnectionFactory();
    }
 
    @Bean(name="idsSerializationContext")
    public RedisSerializationContext<String, Ids> idsReactiveRedisTemplate() {
        Jackson2JsonRedisSerializer<Ids> serializer = new Jackson2JsonRedisSerializer<>(Ids.class);
        RedisSerializationContext.RedisSerializationContextBuilder<String, Ids> builder =
                RedisSerializationContext.newSerializationContext(ew StringRedisSerializer());
        return builder.value(serializer).hashValue(serializer).hashKey(serializer).build();
    }
 
    @Bean(name="sampleSerializationContext")
    public RedisSerializationContext<String, Sample> sampleReactiveRedisTemplate() {
        Jackson2JsonRedisSerializer<Sample> serializer = new Jackson2JsonRedisSerializer<>(Sample.class);
        RedisSerializationContext.RedisSerializationContextBuilder<String, Sample> builder =
                RedisSerializationContext.newSerializationContext(new StringRedisSerializer());
        return builder.value(serializer).hashValue(serializer).hashKey(serializer).build();
    }
}

Repository 구성

Spring WebFlux와 ReactiveRedisTemplate을 이용을 사용하면 Async Non-Blocking 처리로 I/O 밀집적인 작업을 효율적으로 처리 가능하여 대량의 동시 요청을 효과적으로 처리하고 확장 가능한 애플리케이션을 개발할 수 있다. 또한, Redis 데이터베이스에 비동기 쿼리를 보내고 전반적인 성능을 향상시킬 수 있어 데이터 스트리밍과 실시간 데이터 처리에 적합하다. 그리고, Spring 프레임워크와의 원활한 통합으로 풍부한 개발 환경을 제공하며 모델의 일관성을 유지하고 유지 관리를 간편하게 할 수 있다.

실행환경 라이브러리

@Repository 클래스에 EgovRedisRepository 클래스를 extends 하여 insertData, updateData, deleteData, selectData 메소드를 활용한다.

package org.egovframe.rte.psl.reactive.redis.repository;
 
public class EgovRedisRepository<T> extends ReactiveRedisTemplate {
    public EgovRedisRepository(ReactiveRedisConnectionFactory connectionFactory, RedisSerializationContext serializationContext) {
        super(connectionFactory, serializationContext);
    }
 
    public Flux<T> selectData(String redisKey) {
        return opsForList().range(redisKey, 0, -1);
    }
 
    public Mono<Long> findIndex(String redisKey, T entity) {
        return opsForList().indexOf(redisKey, entity);
    }
 
    public Mono<Long> countData(String redisKey) {
        return opsForList().size(redisKey);
    }
 
    public Mono<T> insertData(String redisKey, T entity) {
        return opsForList().leftPush(redisKey, entity);
    }
 
    public Mono<T> updateData(String redisKey, long idx, T entity) {
        return opsForList().set(redisKey, idx, entity);
    }
 
    public Mono<Boolean> deleteAllData(String redisKey) {
        return opsForList().delete(redisKey);
    }
 
    public Mono<Boolean> deleteData(String redisKey, T entity) {
        return opsForList().remove(redisKey, 0, entity);
    }
}

참고자료

6 - 연계통합

연계통합 레이어는 타 시스템과의 연동기능을 지원한다.

연계통합

연계통합 레이어는 타 시스템과의 연동기능을 지원한다.

6.1 - Naming 서비스

Naming 서비스는 JNDI API를 통해 자원을 찾고, 다른 애플리케이션에서 사용할 수 있도록 Naming 서버에 자원을 등록 및 검색할 수 있도록 지원하는 서비스이다. 이를 통해 애플리케이션 간 자원 공유와 접근이 가능해진다.

Naming Service

개요

Naming 서비스는 Java Naming and Directory Interface(JNDI) API를 이용하여 자원(Resource)를 찾을 수 있도록 도와주는 서비스이다. Naming 서비스를 지원하는 Naming 서버에 자원을 등록하여 다른 어플리케이션에서 사용할 수 있도록 공개하고, Naming 서버에 등록되어 있는 자원을 찾아와서 이용할 수 있게 한다.

Naming Service

주요 개념

Java Naming and Directory Interface(JNDI)

Java Naming and Directory Interface(JNDI)는 Java 소프트웨어 클라이언트가 이름(name)을 이용하여 데이터 및 객체를 찾을 수 있도록 도와주는 디렉토리 서비스에 대한 Java API이다.

설명

Naming 서비스는 사용하는 방식에는 Spring XML Configuration 파일에 설정하는 방식과 JNDI API를 wrapping한 JndiTemplate class를 사용하는 방식이 있다.

Spring XML Configuration 설정

Spring Framework는 XML Configuration 파일에 JNDI 객체를 설정할 수 있다. 단, 설정 파일을 통해서는 JNDI 객체를 lookup하는 것만 가능하므로, bind, rebind, unbind 기능을 사용하려면 Using JndiTemplate 방식을 사용해야 한다.

Spring Framework은 XML Configuration을 간편하게 할 수 있게 하기 위해 2.0 버전부터 jee tag를 제공하고 있다. 전자정부 개발프레임워크는 Spring 2.5 이상을 기반으로 하기 때문에 본 가이드는 jee tag를 사용한 방식만을 설명한다.

설정

jee tag를 사용하기 위해서는 Spring XML Configuration 파일의 머릿말에 namespace와 schemaLocation를 추가해야 한다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd">
 
    <!-- <bean/> definitions here -->
 
</beans>

jndi-lookup tag

jndi-lookup tag는 JNDI 객체를 찾아서 bean으로 등록해주는 tag이다.

tag 설명
    <jee:jndi-lookup id="bean id"
                     jndi-name="jndi name"
                     cache="true or false"
                     resource-ref="true or false"
                     lookup-on-startup="true or false"
                     expected-type="java class"
                     proxy-interface="java class">
        <jee:environment>
            name=value
            ping=pong
            ...
        </jee:environment>
    </jee:jndi-lookup>

jndi-lookup tag는 Spring Framework의 JndiObjectFactoryBean class와 1:1로 매핑된다. tag의 attribute 값은 다음과 같다.

Attribute
설명
Optional
Data Type
Default 값
비고
idSpring XML Configuration의 bean id이다.NString
jndi-name찾고자 하는 JNDI 객체의 이름이다.NString
cache한번 찾은 JNDI 객체에 대한 cache여부를 나타낸다.Ybooleantrue
resource-refJ2EE Container 내에서 찾을지 여부를 나타낸다.Ybooleanfalse
lookup-on-startup시작시에 lookup을 수행할지 여부를 타나낸다.Ybooleantrue
expected-type찾는 JNDI 객체를 assign할 타입을 나타낸다.YClass값이 지정되지 않았을 경우 무시한다.
proxy-interfaceJNDI 객체를 사용하기 위한 Proxy Interface이다.YClass값이 지정되지 않았을 경우 무시한다.

jndi-lookup tag의 element인 environment tag는 JNDI Environment 변수값을 등록할 때 사용한다. environment tag는 ‘foo=bar’ 와 같이 <변수명>=<변수값> 형태의 List를 값으로 가진다.

예제
  • Simple

    가장 단순한 설정으로 이름만을 사용하여 JNDI 객체를 찾아준다. 아래 이름 “jdbc/MyDataSource”로 등록되어 있는 JNDI 객체를 찾아 “userDao” Bean의 “dataSource” property로 Dependency Injection하는 예제이다.

      <jee:jndi-lookup id="dataSource" jndi-name="jdbc/MyDataSource"/>
    
      <bean id="userDao" class="com.foo.JdbcUserDao">
          <!-- Spring will do the cast automatically (as usual) -->
          <property name="dataSource" ref="dataSource"/>
      </bean>
    
  • With single JNDI environment settings

    아래는 단일 JNDI 환경 설정을 사용하여 JNDI 객체를 찾아오는 예제이다.

      <jee:jndi-lookup id="dataSource" jndi-name="jdbc/MyDataSource">
          <jee:environment>foo=bar</jee:environment>
      </jee:jndi-lookup>
    
  • With multiple JNDI environment settings

    아래는 복수 JNDI 환경 설정을 사용하여 JNDI 객체를 찾아오는 예제이다.

      <jee:jndi-lookup id="dataSource" jndi-name="jdbc/MyDataSource">
          <!-- newline-separated, key-value pairs for the environment (standard Properties format) -->
          <jee:environment>
              foo=bar
              ping=pong
          </jee:environment>
      </jee:jndi-lookup>
    
  • Complex

    아래는 이름 외 다양한 설정을 통해 JNDI 객체를 찾아오는 예제이다.

      <jee:jndi-lookup id="dataSource"
                  jndi-name="jdbc/MyDataSource"
                  cache="true"
                  resource-ref="true"
                  lookup-on-startup="false"
                  expected-type="com.myapp.DefaultFoo"
                  proxy-interface="com.myapp.Foo"/>
    

local-slsb tag

local-slsb tag는 EJB Stateless SessionBean을 참조하기 위한 tag이다.

tag 설명
    <jee:local-slsb id="bean id"
                    jndi-name="JNDI name"
                    business-interface="Java Class"
                    cache-home="true or false"
                    lookup-home-on-startup="true or false"
                    resource-ref="true or false">
        <jee:environment>
            name=value
            ping=pong
            ...
        </jee:environment>
    </jee:local-slsb>

locak-slsb tag는 Spring Framework의 LocalStatelessSessionProxyFactoryBean class와 1:1로 매핑된다. tag의 attribute는 다음과 같다.

Attribute
설명
Optional
Data Type
Default 값
비고
idSpring XML Configuration의 bean id이다.NString
jndi-name찾고자 하는 EJB의 JNDI 이름이다.NString
business-interfaceProxing할 EJB의 Business interface이다.NClass
cache-home한번 찾은 EJB Home 객체에 대한 cache여부를 나타낸다.Ybooleantrue
lookup-home-on-startup시작 시에 lookup을 수행할지 여부를 나타낸다.Ybooleantrue
resource-refJ2EE Container 내에서 찾을지 여부를 나타낸다.Ybooleanfalse

local-slsb tag의 element인 environment tag는 JNDI Environment 변수값을 등록할 때 사용한다. evironment tag는 ‘foo=bar’ 와 같이 <변수명>=<변수값> 형태의 List를 값으로 가진다.

예제
  • Simple

    간단히 사용하는 예제이다.

      <jee:local-slsb id="simpleSlsb" jndi-name="ejb/RentalServiceBean"
          business-interface="com.foo.service.RentalService"/>
    
  • Complex

    local-slsb tag를 사용하기 위해 다양한 설정 값을 이용하는 예제이다.

      <jee:local-slsb id="complexLocalEjb"
          jndi-name="ejb/RentalServiceBean"
          business-interface="com.foo.service.RentalService"
          cache-home="true"
          lookup-home-on-startup="true"
          resource-ref="true"/>
    

remote-slsb tag

remote-slsb tag는 remote EJB Stateless SessionBean을 참조하기 위한 tag이다.

tag 설명
    <jee:remote-slsb id="bean id"
                     jndi-name="JNDI name"
                     business-interface="Java Class"
                     cache-home="true or false"
                     lookup-home-on-startup="true or false"
                     resource-ref="true or false"
                     home-interface="Java Class"
                     refresh-home-on-connect-failure="true or false">
        <jee:environment>
            name=value
            ping=pong
            ...
        </jee:environment>
    </jee:remote-slsb>

remote-slsb tag는 Spring Framework의 SimpleRemoteStatelessSessionProxyFactoryBean class와 1:1로 매핑된다. tag의 attribute는 아래와 같다.

Attribute
설명
Optional
Data Type
Default 값
비고
idSpring XML Configuration의 bean id이다.NString
jndi-name찾고자 하는 EJB의 JNDI 이름이다.NString
business-interfaceProxing할 EJB의 Business interface이다.NClass
cache-home한번 찾은 EJB Home 객체에 대한 cache여부를 나타낸다.Ybooleantrue
lookup-home-on-startup시작 시에 lookup을 수행할지 여부를 나타낸다.Ybooleantrue
resource-refJ2EE Container 내에서 찾을지 여부를 나타낸다.Ybooleanfalse
home-interfaceEJB Home interface이다.YClass
refresh-home-on-connect-failure연결 실패 시, EJB Home을 reflesh할지 여부를 나타낸다.Ybooleanfalse

remote-slsb tag의 element인 environment tag는 JNDI Environment 변수값을 등록할 때 사용한다. evironment tag는 ‘foo=bar’ 와 같이 <변수명>=<변수값> 형태의 List를 값으로 가진다.

예제
  • Simple

    간단히 사용하는 예제이다.

      <jee:remote-slsb id="complexRemoteEjb"
                       jndi-name="ejb/MyRemoteBean"
                       business-interface="com.foo.service.RentalService"
                       home-interface="com.foo.service.RentalService"/>
    
  • Complex

    remove-slsb tag를 사용하기 위해 다양한 설정 값을 이용하는 예제이다.

      <jee:remote-slsb id="complexRemoteEjb"
                       jndi-name="ejb/MyRemoteBean"
                       business-interface="com.foo.service.RentalService"
                       cache-home="true"
                       lookup-home-on-startup="true"
                       resource-ref="true"
                       home-interface="com.foo.service.RentalService"
                       refresh-home-on-connect-failure="true"/>
    

JndiTemplate 클래스 사용

JndiTemplate class는 JNDI API를 쉽게 사용할 수 있도록 제공하는 wrapper class이다.

bind

아래 JndiTemplateSample class의 bind 메소드는 JndiTemplate을 이용하여 argument ‘resource’를 argument ’name’으로 JNDI 객체로 bind한다.

import javax.naming.NamingException;
 
import org.springframework.jndi.JndiTemplate;
 
...
 
public class JndiTemplateSample
{
    private JndiTemplate jndiTemplate = new JndiTemplate();
 
    ...
 
    public boolean bind(final String name, Object resource)
    {
        try
        {
            jndiTemplate.bind(name, resource);
            return true;
        }
        catch (NamingException e)
        {
            e.printStackTrace();
            return false;
        }
    }
 
    ...
}

lookup

JndiTemplate을 이용하여 argument ’name’으로 등록되어 있는 자원(resource)를 찾을 수 있다.

    public Object lookupResource(final String name)
    {
        try
        {
            return jndiTemplate.lookup(name);
        }
        catch (NamingException e)
        {
            e.printStackTrace();
            return null;
        }
    }

lookup with requiredType

JndiTemplate의 lookup 메소드는 찾고자 하는 자원의 이름 뿐 아니라 원하는 타입(Type)을 지정할 수 있다.

    public Foo lookupFoo(final String fooName)
    {
        try
        {
            return jndiTemplate.lookup(fooName, Foo.class);
        }
        catch (NamingException e)
        {
            e.printStackTrace();
            return null;
        }
    }

rebind

JndiTemplate의 rebind 메소드를 사용하여 자원을 재등록할 수 있다.

    public boolean rebind(final String name, Object resource)
    {
        try
        {
            jndiTemplate.rebind(name, resource);
            return true;
        }
        catch (NamingException e)
        {
            e.printStackTrace();
            return false;
        }
    }

unbind

JndiTemplate의 unbind 메소드를 사용하여 등록된 자원을 등록해제할 수 있다.

    public boolean unbind(final String name)
    {
        try
        {
            jndiTemplate.unbind(name);
            return true;
        }
        catch (NamingException e)
        {
            e.printStackTrace();
            return false;
        }
    }

참고자료

6.2 - Integration 서비스

Integration 서비스는 전자정부 개발프레임워크 기반의 시스템이 타 시스템과의 연계를 위해 사용하는 Interface의 표준을 정의한 것이다.

Integration 서비스

개요

Integration 서비스는 전자정부 개발프레임워크 기반의 시스템이 타 시스템과의 연계를 위해 사용하는 Interface의 표준을 정의한 것이다.

설명

목적

기존의 전자정부 시스템은 타 시스템과의 연계를 위해 연계 솔루션을 사용하거나 자체 개발한 연계 모듈을 사용해왔다. 기존에 사용된 연계 솔루션 및 자체 연계 모듈은 각각 고유한 설정 및 사용 방식을 가지고 있어, 동일한 연계 서비스라 할지라도 사용하는 연계 모듈에 따라 각기 다른 방식으로 개발되어 왔다. 본 Integration 서비스는 이러한 중복 개발을 없애고, 표준화된 설정 및 사용 방식을 정의하여 개발 효율성을 제고한다.

아키텍처

Integration 서비스를 사용하여 구현된 전자정부 시스템의 아키텍처는 다음과 같다.

Integration Service Architecture

Integration 서비스는 연계 서비스 요청 Interface, 연계 서비스 제공 Interface, 연계 메시지 및 메시지 헤더 등을 정의하고 있으며, 연계 서비스 요청 모듈 및 제공 모듈은 연계 Adaptor나 연계 솔루션과 관계 없이 Integration 서비스가 제공하는 Interface와 Class만을 사용하여 연계 업무를 수행할 수 있다. Integration 서비스는 연계 Interface 외에 연계에 필요한 정보를 담기 위한 Metadata를 정의하고 있다. Metadata는 연계 Interface를 사용하기 위해 필요한 최소한의 정보(연계 기관 정보, 연계 시스템 정보, 연계 서비스 정보, 메시지 형식 등)을 정의하고 있다.

구성

Integration 서비스는 연계에 필요한 정보를 정의한 Metadata와 연계 서비스를 사용 및 제공하기 위한 API로 구성된다.

6.3 - Integration 서비스 Metadata

Integration 서비스 Metadata는 연계에 필요한 정보를 정의하며, API 사용에 앞서 연계등록정보와 기관, 시스템, 서비스 등의 메타데이터를 이해하는 것이 중요하다. 실제 연계 Adaptor 사용법은 연계 서비스 API에서 설명된다.

Metadata

개요

Integration 서비스 Metadata는 연계에 필요한 정보를 정의하고 있다. 본 장은 실제 Integration 서비스로 구현된 연계 Adaptor를 사용하는 방식에 직접적인 도움을 주지는 않는다. 실제 사용법은 연계 서비스 API에서 설명하고 있다. 단, 연계 서비스 API의 핵심 Interface인 EgovIntegrationService의 단위에 해당하는 연계등록정보와, 이와 관련된 기관, 시스템, 서비스 등의 Metadata를 이해하는 것은 API 사용에 도움이 될 수 있다.

설명

논리모델

Integration 서비스 Metadata의 논리모델은 연계를 위해 필요한 논리적인 정보를 정의한다.

논리ERD

Integration 서비스 Metadata의 논리ERD 및 Entity 설명은 다음과 같다.

  • ERD의 Entity attribute의 Notation은 "<name> : <data type> <domain>" 이다.

Metadata Logical ERD

Entity
설명
기관연계 서비스를 제공 또는 사용하는 기관을 나타낸다.
하나의 기관은 다수의 시스템을 가지고 있다.
시스템연계 서비스를 제공 또는 사용하는 시스템을 나타낸다.
하나의 시스템은 반드시 하나의 기관에 속하며, 다수의 서비스를 가지고 있다.
서비스연계 서비스를 제공하는 단위를 나타낸다.
하나의 서비스는 반드시 하나의 시스템에 속한다.
연계등록정보연계 서비스를 사용하기 위한 단위를 나타낸다.
연계 요청 시스템이 연계 제공 서비스를 사용하기 위해서 등록해야 하는 정보를 담고 있다.
레코드타입연계에 사용되는 메시지의 형태를 나타낸다.
<Key, Value> 쌍의 정보를 담고 있는 레코드 형태의 타입을 정의하고 있다.
하나의 레코드타입은 다수의 레코드필드를 가지고 있다.
레코드필드레코드타입에 속하는 내부 필드의 정의를 나타낸다.
필드의 이름과 타입을 정의한다.
하나의 레코드필드는 반드시 하나의 레코드타입에 속한다.

논리모델 Domain 설명

Domain
Data Type
설명
비고
기관IDCHAR(8)연계 서비스를 제공 또는 사용하는 기관의 ID를 나타낸다.ID 체계 참조
시스템IDCHAR(8)연계 서비스를 제공 또는 사용하는 시스템의 ID를 나타낸다.ID 체계 참조
서비스IDCHAR(8)연계 서비스의 ID를 나타낸다.ID 체계 참조
타입IDVARCHAR(40)메시지를 구성하는 각 구성 요소의 형식인 타입의 ID를 나타낸다.ID 체계 참조
모듈IDVARCHAR(40)연계 서비스를 제공하기 위해 등록되어 있는 서비스 제공 모듈의 ID를 나타낸다.ID 체계 참조
BooleanCHAR(1)참/거짓(true/false) 등의 논리값을 나타낸다.‘Y’ : true
‘N’ : false
NumberINTEGER일반적인 정수를 나타낸다.
일시DATETIME일자와 시각을 포함하는 일시를 나타낸다.
이름VARCHAR(40)기관, 시스템, 서비스 등의 각종 이름을 나타낸다.

논리모델 Entity 설명

기관
Entity 명기관
설명연계 서비스를 제공 또는 사용하는 기관을 나타낸다.
하나의 기관은 다수의 시스템을 가지고 있다.
Attribute
SeqPKAttribute명DomainData Type설명
1Y기관ID기관IDCHAR(8)기관의 ID이다.
2기관명이름VARCHAR(40)기관의 이름이다.
시스템
Entity 명시스템
설명연계 서비스를 제공 또는 사용하는 시스템을 나타낸다.
하나의 시스템은 반드시 하나의 기관에 속하며, 다수의 서비스를 가지고 있다.
Attribute
SeqPKAttribute명DomainData Type설명
1Y기관ID기관IDCHAR(8)기관의 ID이다.
2Y시스템ID시스템IDCHAR(8)시스템의 ID이다.
3시스템명이름VARCHAR(40)시스템의 이름이다.
4표준여부BooleanCHAR(1)표준 준수 여부를 나타낸다.
서비스
Entity 명서비스
설명연계 서비스를 제공하는 단위를 나타낸다.
하나의 서비스는 반드시 하나의 시스템에 속한다.
Attribute
SeqPKAttribute명DomainData Type설명
1Y기관ID기관IDCHAR(8)기관의 ID이다.
2Y시스템ID시스템IDCHAR(8)시스템의 ID이다.
3Y서비스ID서비스IDCHAR(8)서비스의 ID이다.
4서비스명이름VARCHAR(40)서비스의 이름이다.
5요청메시지타입ID타입IDVARCHAR(40)요청 메시지의 타입 ID이다.
6응답메시지타입ID타입IDVARCHAR(40)응답 메시지의 타입 ID이다.
7응답모듈ID모듈IDVARCHAR(40)실제 서비스를 제공하는 응답 모듈의 ID이다.
8표준여부BooleanCHAR(1)표준 준수 여부를 나타낸다.
9사용여부BooleanCHAR(1)서비스의 사용 여부를 나타낸다.
연계등록정보
Entity 명연계등록정보
설명연계 서비스를 사용하기 위한 단위를 나타낸다.
연계 요청 시스템이 연계 제공 서비스를 사용하기 위해서 등록해야 하는 정보를 담고 있다.
Attribute
SeqPKAttribute명DomainData Type설명
1Y제공기관ID기관IDCHAR(8)서비스를 제공하는 기관의 ID이다.
2Y제공시스템ID시스템IDCHAR(8)서비스를 제공하는 시스템의 ID이다.
3Y제공서비스ID서비스IDCHAR(8)서비스를 제공하는 서비스의 ID이다.
4Y요청기관ID기관IDCHAR(8)서비스를 요청하는 기관의 ID이다.
5Y요청시스템ID시스템IDCHAR(8)서비스를 요청하는 시스템의 ID이다.
6DefaultTimeoutNumberINTEGER서비스 요청 시 사용되는 default timeout 값이다.
7사용여부BooleanCHAR(1)등록된 연계의 사용 여부를 나타낸다.
8유효시작일시일시DATEIME등록된 연계가 유효한 기간의 시작 일시를 나타낸다.
9유효종료일시일시DATEIME등록된 연계가 유효한 기간의 종료 일시를 나타낸다.
레코드타입
Entity 명레코드타입
설명연계에 사용되는 메시지의 형태를 나타낸다.
<Key, Value> 쌍의 정보를 담고 있는 레코드 형태의 타입을 정의하고 있다.
하나의 레코드타입은 다수의 레코드필드를 가지고 있다.
Attribute
SeqPKAttribute명DomainData Type설명
1Y레코드타입ID타입IDVARCHAR(40)레코드 타입의 ID이다.
2레코드타입명이름VARCHAR(40)레코드 타입의 이름이다.
3부모레코드타입ID타입IDVARCHAR(40)부모 레코드 타입의 ID이다.
레코드필드
Entity 명레코드필드
설명레코드타입에 속하는 내부 필드의 정의를 나타낸다.
필드의 이름과 타입을 정의한다.
하나의 레코드필드는 반드시 하나의 레코드타입에 속한다.
Attribute
SeqPKAttribute명DomainData Type설명
1Y레코드타입ID타입IDVARCHAR(40)필드가 속한 레코드의 타입 ID이다.
2Y필드명이름VARCHAR(40)필드의 이름이다.
3필드타입ID타입IDVARCHAR(40)필드의 타입 ID이다.

물리모델

Integration 서비스 Metadata의 물리모델은 논리모델을 실제 물리적인 DB로 구현하기 위한 모델로서, 아래 물리ERD는 Oracle DB를 가정하여 작성된 것이다. 물리모델은 Hibernate 등과 같은 Object Relational Mapping(ORM)을 사용하여 Access하는 것을 고려하여, 복수의 Attribute를 Identifier로 갖는 Entity를 Table로 변환할 때 Surrogate Key를 도입하고, 기존 Identifier는 Unique Constraints로 적용하여 정의되었다.

물리ERD

Integration 서비스 Metadata의 물리ERD 및 Table 설명은 다음과 같다.

  • ERD의 Table Column의 Notation은 "<name> : <data type> <domain> <null option> <key>*" 이다.

  • 물리ERD의 경우, 각 연계 Adaptor 또는 연계 솔루션, 또는 시스템에 따라 사용하는 DB가 달라지므로 Data Type 등이 변경될 수 있다.

Metadata Physical ERD

Table
Entity
설명
ORGANIZATION기관연계 서비스를 제공 또는 사용하는 기관을 나타낸다.
하나의 기관은 다수의 시스템을 가지고 있다.
SYSTEM시스템연계 서비스를 제공 또는 사용하는 기관을 나타낸다.
하나의 기관은 다수의 시스템을 가지고 있다.
SERVICE서비스연계 서비스를 제공하는 단위를 나타낸다.
하나의 서비스는 반드시 하나의 시스템에 속한다.
INTEGRATION연계등록정보연계 서비스를 사용하기 위한 단위를 나타낸다.
연계 요청 시스템이 연계 제공 서비스를 사용하기 위해서 등록해야 하는 정보를 담고 있다.
RECORD_TYPE레코드타입연계에 사용되는 메시지의 형태를 나타낸다.
<Key, Value> 쌍의 정보를 담고 있는 레코드 형태의 타입을 정의하고 있다.
하나의 레코드타입은 다수의 레코드필드를 가지고 있다.
RECORD_TYPE_FIELD레코드필드레코드타입에 속하는 내부 필드의 정의를 나타낸다.
필드의 이름과 타입을 정의한다.
하나의 레코드필드는 반드시 하나의 레코드타입에 속한다.

물리 모델 Domain 설명

Domain
Data Type
설명
비고
OrganizationIdCHAR(8)연계 서비스를 제공 또는 사용하는 기관의 ID를 나타낸다.ID 체계 참조
SystemIdCHAR(8)연계 서비스를 제공 또는 사용하는 시스템의 ID를 나타낸다.ID 체계 참조
ServiceIdCHAR(8)연계 서비스의 ID를 나타낸다.ID 체계 참조
TypeIdVARCHAR2(40)메시지를 구성하는 각 구성 요소의 형식인 타입의 ID를 나타낸다.ID 체계 참조
BeanIdVARCHAR2(40)연계 서비스를 제공하기 위해 등록되어 있는 서비스 제공 모듈의 ID를 나타낸다.ID 체계 참조
BooleanCHAR(1)참/거짓(true/false) 등의 논리값을 나타낸다.‘Y’ : true
‘N’ : false
NumberINTEGER일반적인 정수를 나타낸다.
DatetimeDATEIME일자와 시각을 포함하는 일시를 나타낸다.
NameVARCHAR2(40)기관, 시스템, 서비스 등의 각종 이름을 나타낸다.
SurrogateKeyVARCHAR2(20)Composite 형태의 Primary Key를 대체하기 위한 키

물리 모델 Table 설명

ORGANIZATION
Table 명ORGANIZATIONEntity기관
설명연계 서비스를 제공 또는 사용하는 기관을 나타낸다.
하나의 기관은 다수의 시스템을 가지고 있다.
Column
SeqPKColumn명한글명DomainData TypeNull설명
1YORGANIZATION_ID기관IDOrganizationIdCHAR(8)N기관의 ID이다.
2ORGANIZATION_NAME기관명NameVARCHAR2(40)N기관의 이름이다.
Constraints
PRIMARY KEY (ORGANIZATION_ID)
SYSTEM
Table 명SYSTEMEntity시스템
설명연계 서비스를 제공 또는 사용하는 시스템을 나타낸다.
하나의 시스템은 반드시 하나의 기관에 속하며, 다수의 서비스를 가지고 있다.
Column
SeqPKColumn명한글명DomainData TypeNull설명
1YSYSTEM_KEY시스템KEYSurrogateKeyVARCHAR2(20)N시스템의 Surrogate Key이다.
2ORGANIZATION_ID기관IDOrganizationIdCHAR(8)N기관의 ID이다.
3SYSTEM_ID시스템IDSystemIdCHAR(8)N시스템의 ID이다.
4SYSTEM_NAME시스템명NameVARCHAR2(40)N시스템의 이름이다.
5STANDARD_YN표준여부BooleanCHAR(1)N표준 준수 여부를 나타낸다.
Constraints
PRIMAEY KEY (SYSTEM_KEY)
UNIQUE (ORGANIZATION_ID, SYSTEM_ID)
FOREIGN KEY (ORGANIZATION_ID) REFERENCES ORGANIZATION (ORGANIZATION_ID)
SERVICE
Table 명SERVICEEntity서비스
설명연계 서비스를 제공하는 단위를 나타낸다.
하나의 서비스는 반드시 하나의 시스템에 속한다.
Column
SeqPKColumn명한글명DomainData TypeNull설명
1YSERVICE_KEY서비스KEYSurrogateKeyVARCHAR2(20)N서비스의 Surrogate Key이다.
2SYSTEM_KEY시스템KEYSurrogateKeyVARCHAR2(20)N시스템의 Surrogate Key이다.
3SERVICE_ID서비스IDServiceIdCHAR(8)N서비스의 ID이다.
4SERVICE_NAME서비스명NameVARCHAR2(40)N서비스의 이름이다.
5REQUEST_MESSAGE_TYPE_ID요청메시지타입IDTypeIdVARCHAR2(40)N요청 메시지의 타입 ID이다.
6RESPONSE_MESSAGE_TYPE_ID응답메시지타입IDTypeIdVARCHAR2(40)N응답 메시지의 타입 ID이다.
7SERVICE_PROVIDER_BEAN_ID응답모듈IDBeanIdVARCHAR2(40)Y실제 서비스를 제공하는 응답 모듈의 ID이다.
8STANDARD_YN표준여부BooleanCHAR(1)N표준 준수 여부를 나타낸다.
9USING_YN사용여부BooleanCHAR(1)N서비스의 사용 여부를 나타낸다.
Constraints
PRIMARY KEY (SERVICE_KEY)
UNIQUE (SYSTEM_KEY, SERVICE_ID)
FOREIGN KEY (SYSTEM_KEY) REFERENCES SYSTEM (SYSTEM_KEY)
INTEGRATION
Table 명INTEGRATIONEntity연계등록정보
설명연계 서비스를 사용하기 위한 단위를 나타낸다.
연계 요청 시스템이 연계 제공 서비스를 사용하기 위해서 등록해야 하는 정보를 담고 있다.
Column
SeqPKColumn명한글명DomainData TypeNull설명
1YINTEGRATION_ID연계IDSurrogate KeyVARCHAR2(20)N연계등록정보의 Surrogate Key이다.
2PROVIDER_SERVICE_KEY제공서비스KEYSurrogate KeyVARCHAR2(20)N서비스를 제공하는 서비스의 Surrogate Key이다.
3CONSUMER_SYSTEM_KEY요청시스템KEYSurrogate KeyVARCHAR2(20)N서비스를 요청하는 시스템의 Surrogate Key이다.
4DEFAULT_TIMEOUTDefaultTimeoutNumberINTEGERN서비스 요청 시 사용되는 default timeout 값이다.
5USING_YN사용여부BooleanCHAR(1)N등록된 연계의 사용 여부를 나타낸다.
6VALIDATE_FROM유효시작일시DatetimeDATETIMEY등록된 연계가 유효한 기간의 시작 일시를 나타낸다.
7VALIDATE_TO유효종료일시DatetimeDATETIMEY등록된 연계가 유효한 기간의 종료 일시를 나타낸다.
Constraints
PRIMARY KEY (INTEGRATION_ID)
UNIQUE (PROVIDER_SERVICE_KEY, CONSUMER_SYSTEM_KEY)
FOREIGN KEY (PROVIDER_SERVICE_KEY) REFERENCES SERVICE (SERVICE_KEY)
ROREIGN KEY (CONSUMER_SYSTEM_KEY) REFERENCES SYSTEM (SYSTEM_KEY)
RECORD_TYPE
Table 명RECORD_TYPEEntity레코드타입
설명연계에 사용되는 메시지의 형태를 나타낸다.
<Key, Value> 쌍의 정보를 담고 있는 레코드 형태의 타입을 정의하고 있다.
하나의 레코드타입은 다수의 레코드필드를 가지고 있다.
Column
SeqPKColumn명한글명DomainData TypeNull설명
1YRECORD_TYPE_ID레코드타입IDTypeIdVARCHAR2(40)N레코드 타입의 ID이다.
2RECORD_TYPE_NAME레코드타입명NameVARCHAR2(40)N레코드 타입의 이름이다.
3PARENT_RECORD_TYPE_ID부모레코드타입IDTypeIdVARCHAR2(40)Y부모 레코드 타입의 ID이다.
Constraints
PRIMARY KEY (RECORD_TYPE_ID)
FOREIGN KEY (PARENT_RECORD_TYPE_ID) REFERENCES RECORD_TYPE (RECORD_TYPE_ID)
RECORD_TYPE_FIELD
Table 명RECORD_TYPE_FIELDEntity레코드필드
설명레코드타입에 속하는 내부 필드의 정의를 나타낸다.
필드의 이름과 타입을 정의한다.
하나의 레코드필드는 반드시 하나의 레코드타입에 속한다.
Column
SeqPKColumn명한글명DomainData TypeNull설명
1YRECORD_TYPE_ID레코드타입IDTypeIdVARCHAR2(40)N필드가 속한 레코드의 타입 ID이다.
2YRECORD_FIELD_NAME필드명NameVARCHAR2(40)N필드의 이름이다.
3RECORD_FIELD_TYPE_ID필드타입IDTypeIdVARCHAR2(40)N필드의 타입 ID이다.
Constraints
PRIMARY KEY (RECORD_TYPE_ID, RECORD_FIELD_NAME)
FOREIGN KEY (RECORD_TYPE_ID) REFERENCES RECORD_TYPE (RECORD_TYPE_ID)

Metadata Java Classes

Integration 서비스의 Metadata를 읽어오기 위한 Java Class를 정의하고 있다.

ClassDiagram

ClassDiagram

ID 체계

Integration 서비스 Metadata의 ID 체계는 다음과 같다.

ID
영문명
Data Type
형태
제약사항
기관IDOrganizationIdCHAR(8)기관을 의미하는 Prefix ‘ORG’ 이후에 숫자 ‘0’으로 채워진 5자리 정수
Ex) ORG00000, ORG00001, ORG99999
• 모든 기관은 반드시 Unique한 ID를 가져야 한다.
시스템IDSystemIdCHAR(8)시스템을 의미하는 Prefix ‘SYS’ 이후에 숫자 ‘0’으로 채워진 5자리 정수
Ex) SYS00000, SYS00001, SYS99999
• 동일한 기관에 속한 모든 시스템은 반드시 Unique한 ID를 가져야 한다. (서로 다른 기관에 속한 시스템은 같은 ID를 가질 수 있다.)
서비스IDServiceIdCHAR(8)서비스를 의미하는 Prefix ‘SRV’ 이후에 숫자 ‘0’으로 채워진 5자리 정수
Ex) SRV00000, SRV00001, SRV99999
• 동일한 시스템에 속한 모든 서비스는 반드시 Unique한 ID를 가져야 한다. (서로 다른 시스템에 속한 시스템은 같은 ID를 가질 수 있다.)
타입IDTypeIdVARCHAR2(40)알파벳 대소문자, 숫자, ‘_’ 로 구성된 길이 1 ~ 40 사이의 문자열
Ex) messageType0000, record_ABC
• 모든 타입은 Unique한 ID를 가져야 한다.
• 메타데이터로 저장되는 레코드타입의 ID는 다음의 Reserved 값을 가질 수 없다. boolean, string, byte, short, integer, long, biginteger, float, double, bigdecimal, calendar
모듈IDBeanIdVARCHAR2(40)알파벳 대소문자, 숫자, ‘_’ 로 구성된 길이 1 ~ 40 사이의 문자열
Ex) serviceProviderA, bean_123
• 모듈ID는 전자정부 개발프레임워크의 기반이 되는 Spring Framework에 등록되는 Bean Id이다.

6.4 - 연계 서비스 API

연계 서비스 API는 연계 서비스를 사용 및 제공하기 위한 interface를 제공한다.

연계 서비스 API

개요

연계 서비스 API는 연계 서비스를 사용 및 제공하기 위한 interface를 제공한다.

설명

구성

연계 서비스 API는 다음과 같이 구성된다.

Integration Service Api ClassDiagram

구성요소
설명
EgovIntegrationContext연계 서비스에 대한 설정 및 EgovIntegrationService 객체를 관리한다.
EgovIntegrationMessage연계 서비스를 통해 주고받는 표준 메시지를 정의한다.
EgovIntegrationMessageHeader연계 서비스를 통해 주고받는 표준 메시지 헤더를 정의한다.
EgovIntegrationMessageHeader::ResultCode연계 서비스 결과 코드를 담고 있는 enumeration이다.
EgovIntegrationService연계 서비스를 호출하기 위해 사용한다.
EgovIntegrationResponse연계 서비스를 비동기 방식으로 호출한 경우, 응답 메시지를 받기 위해 사용한다.
EgovIntegrationServiceCallback연계 서비스를 비동기 방식으로 호출한 경우, 응답 메시지를 받기 위한 Callback interface이다.
EgovIntegrationServiceCallback::CallbackId연계 서비스를 Callback을 이용한 비동기 방식으로 호출한 경우, 요청 메시지와 응답 메시지를 연결하기 위한 ID를 나타내는 interface이다.
EgovIntegrationServiceProvider연계 서비스를 제공하기 위해 사용한다.

EgovIntegrationContext

EgovIntegrationContext는 연계 서비스에 대한 설정 및 EgovIntegrationService 객체를 관리한다. 연계 서비스를 사용하기 위해서는 EgovIntegrationContext의 getService 메소드를 사용하여 EgovIntegrationService 객체를 얻어와야 한다.
아래는 주민등록번호와 성명을 이용하여 실명확인을 수행하는 예제이다.

package itl.sample;
 
import javax.annotation.Resource;
 
import egovframework.rte.itl.integration.EgovIntegrationContext;
import egovframework.rte.itl.integration.EgovIntegrationService;
 
public class EgovIntegrationSample
{
    @Resource(name = "egovIntegrationContext")
    private EgovIntegrationContext egovIntegrationContext;
 
    public boolean verifyName(final String name, final String residentRegistrationNumber)
    {
        // 연계ID가 "INT_VERIFY_NAME"인 연계 서비스 객체를 얻어온다.
        EgovIntegrationService service = egovIntegrationContext.getService("INT_VERIFY_NAME");
 
        // 요청 메시지 생성
         EgovIntegrationMessage requestMessage = service.createRequestMessage();
 
        // 요청 메시지 작성
         requestMessage.getBody().put("name", name);
        requestMessage.getBody().put("residentRegistrationNumber", residentRegistrationNumber);
 
        // 서비스 요청
         EgovIntegrationMessage responseMessage = service.sendSync(requestMessage);
 
        // 결과 return
        return responseMessage.getBody().get("result");
    }
}

위 예제에 해당하는 Metadata는 아래와 같다.

INTEGRATION
IDPROVIDER_SERVICE_KEYCONSUMER_SYSTEM_KEYDEFAULT_TIMEOUTUSING_YNVALIDATE_FROMVALIDATE_TO
'INT_VERIFY_NAME''SERVICE_VERIFY_NAME''SYSTEM_CONSUMER'5000'Y'NULLNULL
ORGANIZATION
IDNAME
'ORG00001''요청 기관'
'ORG00002''제공 기관'
SYSTEM
SYSTEM_KEYORGANIZATION_IDSYSTEM_IDSYSTEM_NAMESTANDARD_YN
'SYSTEM_CONSUMER''ORG00001''SYS00001''요청 시스템''Y'
'SYSTEM_PROVIDER''ORG00002''SYS00001''응답 시스템''Y'
SERVICE
SERVICE_KEYSYSTEM_KEYSERVICE_IDSERVICE_NAMEREQUEST_MESSAGE_TYPE_IDRESPONSE_MESSAGE_TYPE_IDSERVICE_PROVIDER_BEAN_IDUSING_YNSTANDARD_YN
'SERVICE_VERIFY_NAME''SYSTEM_PROVIDER''SRV00001''VerifyName''REQ_VERIFY_NAME''RES_VERIFY_NAME''serviceVerifyName''Y''Y'
RECORD_TYPE
RECORD_TYPE_IDRECORD_TYPE_NAMEPARENT_RECORD_TYPE_ID
'REQ_VERIFY_NAME''RequestVerifyName'NULL
'RES_VERIFY_NAME''ResponseVerifyName'NULL
RECORD_TYPE_FIELD
RECORD_TYPE_IDRECORD_FIELD_NAMERECORD_FIELD_TYPE_ID
'REQ_VERIFY_NAME''name''string'
'REQ_VERIFY_NAME''residentRegistrationNumber''string'
'RES_VERIFY_NAME''result''boolean'

EgovIntegrationMessage

EgovIntegrationMessage는 헤더부, 바디부, 첨부파일로 구성된다.

헤더부

EgovIntegrationMessage의 헤더부를 access 하기 위한 메소드는 아래와 같다.

Method Summary
EgovIntegrationMessageHeadergetHeader()
voidsetHeader(EgovIntegrationMessageHeader header)

바디부

EgovIntegrationMessage의 바디부를 access 하기 위한 메소드를 아래와 같다.

Method Summary
Map<String, Object>getBody()
voidsetBody(Map<String, Object> body)

EgovIntegrationMessage의 바디부는 다음 값들로만 구성될 수 있다.

  • Java Primitive Type의 Wrapper 객체(Boolean, Byte, Short, Integer, Long, Float, Double)
  • BigInteger, BigDecimal
  • String
  • Calendar
  • List<Object>
  • Map<String, Object>

첨부파일

EgovIntegrationMessage의 첨부파일을 access 하기 위한 메소드는 아래와 같다.

Method Summary
Map<String, Object>getAttachments()
voidsetAttachments(Map<String, Object> attachments)
ObjectgetAttachment(String name)
voidputAttachment(String name, Object attachment)
ObjectremoveAttachment(String name)

EgovIntegrationMessageHeader

EgovIntegrationMessageHeader는 다음과 같은 정보를 담고 있다.

Attribute Name
Data Type
설명
IntegrationIdString연계ID
ProviderOrganizationIdString연계 제공 기관ID
ProviderSystemIdString연계 제공 시스템ID
ProviderServiceIdString연계 제공 서비스ID
ConsumerOrganizationIdString연계 요청 기관ID
ConsumerSystemIdString연계 요청 시스템ID
RequestSendTimeCalendar요청 송신 시각
RequestReceiveTimeCalendar요청 수신 시각
ResponseSendTimeCalendar응답 송신 시각
ResponseReceiveTimeCalendar응답 수신 시각
ResultCodeResultCode결과 코드
ResultMessageString결과 메시지

EgovIntegrationMessageHeader는 위 attribute에 대한 get/set 메소드를 정의하고 있다.

EgovIntegrationMessageHeader::ResultCode

EgovIntegrationMessageHeader의 ResultCode Attribute는 다음 연계 서비스 결과 코드 중 하나의 값을 담고 있다.

Code Name
한글명
Value
설명
OK정상 종료“0000”연계가 정상적으로 종료된 경우
TIME_OUTTimeout 발생“0001”연계 수행 중 Client 단에서 Timeout이 발생한 경우
BUSINESS_ERROR업무 오류 발생“0002”연계 수행 중 Server 단에서 업무적인 오류가 발생한 경우
NOT_USABLE_INTEGRATION사용하지 않는 연계“1000”연계 정의(IntegrationDefinition)의 using flag가 false인 경우
INVALID_TIME연계 가용시간이 아님“1001”연계를 요청한 시각이 연계 정의(IntegrationDefinition)의 validateFrom과 validateTo 사이가 아닌 경우
• 연계 가용시각 조건 =
(validateFrom == null || validateFrom.compareTo(now) <= 0) && (validateTo == null || now.compareTo(validateTo) <= 0)
* now : Calendaer = 현재 시각
NOT_USABLE_SERVICE사용하지 않는 서비스“1002”연계 정의(IntegrationDefinition)에 등록된 제공 서비스(ServiceDefinition)의 using flag 값이 false인 경우
UNEXPECTED_CONSUMER기대하지 않은 연계 요청자“1003”연계 제공 시스템(Server)에 등록된 연계 정의(IntegrationDefinition)의 요청 시스템 코드값과 요청 메시지 헤더의 요청 시스템 코드값이 일치하지 않는 경우
UNEXPECTED_PROVIDER기대하지 않은 연계 제공자“1004”연계 제공 시스템(Server)에 등록된 시스템 코드와 요청 메시지 헤더의 제공 시스템 코드값이 일치하지 않는 경우
NO_SUCH_SERVICE제공하지 않는 서비스“1005”연계 제공 시스템(Server)에 등록되어 있지 않은 서비스를 요청한 경우
FAIL_IN_INITIALIZING연계 서비스 초기화 실패“1006”연계 제공 서비스를 초기화하는데 실패한 경우
FAIL_IN_CREATING_REQUEST_MESSAGE요청 메시지 생성 실패“2000”Client에서 요청 메시지를 생성할 때 오류가 발생한 경우
FAIL_IN_SENDING_REQUEST요청 메시지 송신 실패“2001”Client에서 요청 메시지를 송신할 때 오류가 발생한 경우
FAIL_IN_RECEIVING_REQUEST요청 메시지 수신 실패“3000”Server에서 요청 메시지를 수신할 때 오류가 발생한 경우
FAIL_IN_PARSING_REQUEST_MESSAGE요청 메시지 분석 실패“3001”Server에서 요청 메시지를 분석할 때 오류가 발생한 경우
NO_MESSAGE_HEADER_IN_REQUEST요청 메시지 헤더 부재“3002”Server에서 받은 요청 메시지에 표준 메시지 헤더가 존재하지 않는 경우
FAIL_TO_CALL_SERVICE_PROVIDER서비스 제공 모듈 호출 실패“3003”Server에서 서비스 제공 모듈을 호출할 때 오류가 발생한 경우
FAIL_IN_CREATING_RESPONSE_MESSAGE응답 메시지 생성 실패“4000”Server에서 응답 메시지를 생성할 때 오류가 발생한 경우
FAIL_IN_SENDING_RESPONSE응답 메시지 송신 실패“4001”Server에서 응답 메시지를 송신할 때 오류가 발생한 경우
FAIL_IN_RECEIVING_RESPONSE응답 메시지 수신 실패“5000”Client에서 응답 메시지를 수신할 때 오류가 발생한 경우
FAIL_IN_PARSING_RESPONSE_MESSAGE응답 메시지 분석 실패“5001”Client에서 응답 메시지를 분석할 때 오류가 발생한 경우

EgovIntegrationService

EgovIntegrationService를 동기화 방식의 호출과 비동시화 방식의 호출을 지원한다.

동기화 방식

EgovIntegrationService의 sendSync 메소드는 동기화 방식으로 연계 서비스를 호출한다.

Integration Service API SequenceDiagram SendSync

...
 
public class EgovIntegrationSample
{
    ...
 
    public boolean verifyName(final String name, final String residentRegistrationNumber)
    {
        // EgovIntegrationContext에서 EgovIntegrationService 객체를 얻어온 후, 요청 메시지를 생성 및 작성한다.
        ...
 
        // 동기방식으로 연계 서비스 호출 (timeout = 5000 millisecond)
        EgovIntegrationMessage responseMessage = service.sendSync(requestMessage, 5000);
 
        // 응답 결과 처리
        ...
     }
 
     ...
}

EgovIntegrationContext 또는 Metadata의 연계등록정보에 등록된 default timeout 값을 사용할 경우, timeout 값을 생략할 수 있다.

        ...
 
        EgovIntegrationMessage responseMessage = servicd.sendSync(requestMessage);
 
        ...

비동기화 방식

EgovIntegrationService의 sendAsync 메소드는 비동기화 방식으로 연계 서비스를 호출한다. sendAsync 메소드는 두가지 방식이 존재한다.

Using sendAsync with Response

EgovIntegrationServiceResponse를 이용한 비동기 호출 방식이다. Response 방식의 비동기 호출은 연계 서비스를 요청하는 업무 모듈에 응답에 대한 ownership를 가지고 있으며, 응답 결과를 스스로 처리해야 하는 경우 사용한다.

Integration Service API SequenceDiagram SendAsync With Response

...
 
public class EgovIntegrationSample
{
    ...
 
    public boolean verifyName(final String name, final String residentRegistrationNumber)
    {
        // EgovIntegrationContext에서 EgovIntegrationService 객채를 얻어온 후, 요청 메시지를 생성 및 작성한다.
        ...
 
        // 비동기방식으로 연계 서비스 호출
        EgovIntegrationServiceResponse response = service.sendAsync(requestMessage);
 
        // response 객체를 이용하여 응답 메시지를 받기 전에 필요한 업무를 수행
         ...
 
        // response 객체를 이용하여 응답 메시지 수신(timeout = 5000 millisecond)
         EgovIntegrationMessage responseMessage = response.receive(5000);
 
        // 응답 메시지 처리
        ...
    }
 
    ...
}

EgovIntegrationContext 또는 Metadata의 연계등록정보에 등록된 default timeout 값을 사용할 경우, timeout 값을 생략할 수 있다.

        ...
 
        // response 객체를 이용하여 응답 메시지 수신
        EgovIntegrationMessage responseMessage = response.receive();
 
        ...
Using sendAsync with Callback

EgovIntegrationServiceCallback를 이용한 비동기 호출 방식이다. Callback 방식의 비동기 호출은 연계 서비스를 요청하는 업무 모듈은 단지 요청만을 수행하고, 응답에 대한 처리는 Callback 객체에게 위임해도 상관없은 경우 사용한다.

Integration Service API SequenceDiagram SendAsync With Callback

...
 
public class EgovIntegrationSample
{
    ...
 
    @Resource(name = "verifyNameServiceCallback")
    private EgovIntegrationServiceCallback callback;
 
    public boolean verifyName(final String name, final String residentRegistrationNumber)
    {
        // EgovIntegrationContext에서 EgovIntegrationService 객채를 얻어온 후, 요청 메시지를 생성 및 작성한다.
        ...
 
        // 비동기방식으로 연계 서비스 호출
         service.sendSync(requestMessage, callback);
    }
 
    ...
}
package itl.sample;
 
import egovintegration.rte.itl.integration.EgovIntegrationServiceCallback;
import egovintegration.rte.itl.integration.EgovIntegrationServiceCallback.CallbackId;
 
public class VefiryNameServiceCallback
{
    public CallbackId createId(EgovIntegrationService service, EgovIntegrationMessage requestMessage)
    {
        // 본 메소드는 EgovIntegrationService를 구현한 연계 Adaptor 또는 솔루션에서 불리워진다.
        // 서비스와 요청 메시지를 이용하여 CallbackId를 생성하여 return한다.
        // 생성한 CallbackId는 응답 메시지 수신 시, 해당하는 서비스 및 요청 메시지를 식별하기 위해 사용한다.
 
        // CallbackId 생성
        CallbackId callbadkId = ...
 
        return callbackId;
    }
 
    public vod onReceive(CallbackId callbackId, EgovIntegrationMessage responseMessage)
    {
        // 본 메소드는 처리해야 하는 응답 메시지가 도착했을 때, 연계 Adaptor 또는 솔루션에 의해 불리워진다.
 
        // 응답 메시지 처리
         ...
    }
}

EgovIntegrationServiceProvider

EgovIntegrationServiceProvider interface는 연계 서비스를 제공하기 위한 interface로 연계 서비스를 제공하는 모듈은 본 interface를 implements 해야 한다.
아래 예제는 이름과 주민등록번호는 이용하여 실명확인을 수행하는 서비스를 제공하는 업무 모듈과 Spring Framework Configuration XML 파일이다. (Metadata는 EgovIntegrationContext 예제의 설정과 같다.)

package itl.sample;
 
import egovframework.rte.itl.integration.EgovIntegrationMessage;
import egovframework.rte.itl.integration.EgovIntegrationServiceProvider;
 
public class ServiceVerifyName implements EgovIntegrationServiceProvider
{
    public void service(EgovIntegrationMessage requestMessage, EgovIntegrationMessage responseMessage)
    {
        String name = requestMessage.getBody().get("name");
        String residentRegistrationNumber = requestMessage.getBody().get("residentRegistrationNumber");
 
        // 실명 확인
         boolean result = varifyName(name, residentRegistrationNumber);
 
        responseMessage.getBody().put("result", result);
    }
}
    ...
 
    <bean id="serviceVerifyName" class="itl.sample.ServiceVerifyName"/>
 
    ...

6.5 - WebService

WebService는 전자정부 개발프레임워크 Integration 서비스 표준에 따라 WebService를 요청하고 제공하기 위한 Library이다.

WebService

개요

WebService는 전자정부 개발프레임워크 Integration 서비스 표준에 따라 WebService를 요청하고 제공하기 위한 Library이다.

주요 개념

Web Services

W3C는 Web Service를 “네트워크 상에서 발생하는 컴퓨터 간의 상호작용을 지원하기 위한 소프트웨어 시스템”으로 정의하고 있다. 일반적으로 Web Service는 인터넷과 같은 네트워크 상에서 접근되고, 요청된 서비스를 제공하는 원격 시스템에서 수행되는 Web APIs이다.

WebService

사용 오픈소스

Apache CXF

WebService는 Web Service 구현하기 위해서 Apache CXF를 사용한다.

설명

WebService는 Integration Service 표준에 따라 구현한 Library이므로, 본 장에서는 API 등의 사용 방식은 설명하지 않는다. 본 장은 WebService 만을 위한 추가적인 설정 정보를 설명하고, 설정 방법을 가이드한다.

Metadata

WebService는 연계 서비스를 요청하고 제공하기 위한 Web Service Client와 Server 정보를 필요로한다.

물리ERD

WebService Metadata

Table
설명
WEB_SERVICE_SERVER연계 서비스를 Web Service 형태로 공개(publish)하기 위해 필요한 정보를 담고 있다.
WEB_SERVICE_CLIENTWeb Service 형태로 공개(publish)되어 있는 연계 서비스를 호출하기 위해 필요한 정보를 담고 있다.
WEB_SERVICE_MAPPING전자정부 Integration 서비스 표준에 따라 개발된 서비스가 아닌 기존의 Legacy 시스템의 Web Service를 호출하기 위해, 표준 메시지와 Web Service 메시지 간의 mapping 정보를 담고 있다.

물리 모델 Domain 설명

Domain
Data Type
설명
비고
URLVARCHAR2(200)URL을 나타낸다.
WebServiceMappingTypeCHAR(2)Req/Res 구분을 나타낸다.‘REQ’ : Request
‘RES’ : Response

물리 모델 Table 설명

WEB_SERVICE_SERVER
Table 명WEB_SERVICE_SERVER
설명연계 서비스를 Web Service 형태로 공개(publish)하기 위해 필요한 정보를 담고 있다.
Column
SeqPKColumn명한글명DomainData TypeNull설명
1YSERVICE_KEY서비스KEYSurrogateKeyVARCHAR2(20)N서비스 Key이다.
2ADDRESS주소URLVARCHAR2(200)N서비스를 공개할 주소이다.
3NAMESPACE네임스페이스URLVARCHAR2(200)N서비스의 네임스페이스이다.
4SERVICE_NAME서비스명NameVARCHAR2(40)N공개할 때 사용할 서비스의 이름이다.
5PORT_NAME포트명NameVARCHAR2(40)N공개할 때 사용할 포트의 이름이다.
6OPERATION_NAME기능명NameVARCHAR2(40)N공개할 때 사용할 기능의 이름이다.
Constraints
PRIMARY KEY (SERVICE_KEY)
FOREIGN KEY (SERVICE_KEY) REFERENCES SERVICE (SERVICE_KEY)
WEB_SERVICE_CLIENT
Table 명WEB_SERVICE_CLIENT
설명Web Service 형태로 공개(publish)되어 있는 연계 서비스를 호출하기 위해 필요한 정보를 담고 있다.
Column
SeqPKColumn명한글명DomainData TypeNull설명
1YSERVICE_KEY서비스KEYSurrogateKeyVARCHAR2(20)N서비스 Key이다.
2WSDL_ADDRESSWSDL 주소URLVARCHAR2(200)N사용할 서비스의 WSDL 주소이다.
3NAMESPACE네임스페이스URLVARCHAR2(200)N서비스의 네임스페이스이다.
4SERVICE_NAME서비스명NameVARCHAR2(40)N사용할 서비스의 이름이다.
5PORT_NAME포트명NameVARCHAR2(40)N사용할 포트의 이름이다.
6OPERATION_NAME기능명NameVARCHAR2(40)N사용할 기능의 이름이다.
Constraints
PRIMARY KEY (SERVICE_KEY)
FOREIGN KEY (SERVICE_KEY) REFERENCES SERVICE (SERVICE_KEY)
WEB_SERVICE_MAPPING
Table 명WEB_SERVICE_MAPPING
설명전자정부 Integration 서비스 표준에 따라 개발된 서비스가 아닌 기존의 Legacy 시스템의 Web Service를 호출하기 위해, 표준 메시지와 Web Service 메시지 간의 mapping 정보를 담고 있다.
Column
SeqPKColumn명한글명DomainData TypeNull설명
1YSERVICE_KEY서비스KEYSurrogateKeyVARCHAR2(20)N서비스 Key이다.
2YMESSAGE_TYPE메시지타입WebServiceMappingTypeCHAR(3)NReq/Res 구분이다.
3YFIELD_NAME필드명NameVARCHAR2(40)N표준 메시지 Field 이름이다.
4ARGUMENT_INDEX변수순서NumberIntegerNWeb Service 메시지의 변수 순서이다.
5ARGUMENT_NAME변수명NameVARCHAR2(40)NWeb Service 메시지의 변수 이름이다.
6HEADER_YN헤더여부BooleanCHAR(1)NWeb Service 헤더 여부이다.
Constraints
PRIMARY KEY (SERVICE_KEY, MESSAGE_TYPE, FIELD_NAME)
FOREIGN KEY (SERVICE_KEY) REFERENCES WEB_SERVICE_CLIENT (SERVICE_KEY)

설정 방법

WebService를 사용하기 위해 다음의 설정이 필요하다.

  1. pom.xml에 dependency 설정 추가
  2. Spring XML Configuration 설정

pom.xml에 dependency 설정 추가

WebService를 사용하기 위해서 pom.xml의 dependencies tag에 다음 dependency를 추가한다.

  • <version> tag의 값인 ${egovframework.versioin}에는 사용할 egovframework의 version을 기재한다.
    ...
    <dependencies>
        ...
        <dependency>
            <groupId>egovframework.rte</groupId>
            <artifactId>egovframework.rte.itl.webservice</artifactId>
            <version>${egovframework.version}</version>
        </dependency>
        ...
    </dependencies>
    ...

Spring XML Configuration 설정

WebService를 위한 기본적인 설정이 포함된 “context-webservice.xml” 파일을 Spring XML Configuration 파일에 import한다.

    <import resource="classpath:/egovframework/rte/itl/webservice/context/context-webservice.xml"/>

그리고 Context와 DataSource를 등록해야 한다.(DataSource의 경우, 프로젝트에서 사용하는 것이 있을 경우 설정하지 않아도 된다. 단, 반드시 id가 “dataSource”이여야 한다.)

    <!-- EgovWebServiceContext 이다.
         organizationId 와 systemId 는 현재 시스템의 기관ID 및 시스템ID를 넣어야 한다. -->
    <bean id="egovWebServiceContext"
          class="egovframework.rte.itl.webservice.EgovWebServiceContext"
          init-method="init">
        <property name="organizationId" value="ORG_EGOV"/>
        <property name="systemId" value="SYS00001"/>
        <property name="defaultTimeout" value="5000"/>
        <property name="integrationDefinitionDao" ref="integrationDefinitionDao"/>
        <property name="webServiceServerDefinitionDao" ref="webServiceServerDefinitionDao"/>
        <property name="webServiceClientDefinitionDao" ref="webServiceClientDefinitionDao"/>
        <property name="typeLoader" ref="typeLoader"/>
        <property name="classLoader" ref="classLoader"/>
    </bean>
 
    <!-- DataSource 설정이다. 시스템에 맞게 재작성 해야 한다. 아래는 HSQL Sample이다. -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="net.sf.log4jdbc.DriverSpy"/>
        <property name="url" value="jdbc:log4jdbc:hsqldb:hsql://localhost/test"/>
        <property name="username" value="sa"/>
        <property name="password" value=""/>
        <property name="defaultAutoCommit" value="false"/>
        <property name="poolPreparedStatements" value="true"/>
    </bean>

Client 모듈 개발

WebService Client 모듈은 Web Service로 공개된 Integration 서비스 표준에 따라 호출하는 모듈로서, 본 장은 설정 방식을 설명한다. (호출 방식은 연계 서비스 API를 참조한다.)
Client 모듈을 설정하기 위해서는 다음 과정이 필요하다.

  1. Metadata WEB_SERVICE_CLIENT 설정 추가
  2. (Optional) Metadata WEB_SERVICE_MAPPING 설정 추가

Metadata WEB_SERVICE_CLIENT 설정 추가

Client 모듈을 설정하기 위해서는 Metadata의 WEB_SERVICE_CLIENT Table에 설정을 추가해야 한다.
다음과 같이 Integration 서비스의 Metadata인 INTEGRATION Table에 연계등록정보가 설정되어 있다고 가정한다. (* 기관, 시스템, 서비스, 메시지타입 등의 정보는 설정되어 있으며, 개발하는 시스템은 ‘SYSTEM_CONSUMER’라고 가정함)

INTEGRATION
IDPROVIDER_SERVICE_KEYCONSUMER_SYSTEM_KEYDEFAULT_TIMEOUTUSING_YNVALIDATE_FROMVALIDATE_TO
'INT_VERIFY_NAME''SERVICE_VERIFY_NAME''SYSTEM_CONSUMER'5000'Y'NULLNULL

Web Service ‘SERVICE_VERIFY_NAME’를 호출하기 위해서 WEB_SERVICE_CLIENT에 ‘SERVICE_VERIFY_NAME’을 SERVICE_KEY로 갖는 설정을 추가해야 한다.

WEB_SERVICE_CLIENT
SERVICE_KEYWSDL_ADDRESSNAMESPACESERVICE_NAMEPORT_NAMEOPERATION_NAME
'SERVICE_VERIFY_NAME''http://192.168.0.1:8080/Sample/services/VerifyName?wsdl''http://itl/sample/''VerifyNameService''VerifyNamePort''service'

(Optional) Metadata WEB_SERVICE_MAPPING 설정 추가

만약 호출하는 Web Service가 전자정부 Integration 서비스 표준에 따라 개발된 서비스가 아닌 경우, 메시지 헤더부가 다를 수 있어 별도의 Mapping 정보가 필요하다.
전자정부 Integration 서비스 표준은 Web Service Header부에 들어갈 Attribute들이 EgovIntegrationMessageHeader에 정의되어 있고, 바디부는 EgovIntegrationMessage의 body에 정의되어 있으므로 별도의 mapping 정보 없이 header와 body 부의 구분이 가능하지만, 표준을 따르지 않은 Web Service의 경우 EgovIntegrationMessage의 body부에 정의되어 있는 일부 값들을 헤더에 포함시켜야 한다.

WEB_SERVICE_MAPPING Table의 정보는 Integration 서비스 표준에 정의되어 있는 메시지 형태를 기준으로 한다. 서비스 ‘SERVICE_VERIFY_NAME’의 Request Message는 ’name’, ‘residentRegistrationNumber’ 필드를 가지고, Response Message는 ‘result’ 필드를 가진다. 따라서 ‘SERVICE_VERIFY_NAME’에 해당하는 WEB_SERVICE_MAPPING은 다음의 정보를 가져야 한다.

WEB_SERVICE_MAPPING
SERVICE_KEYMESSAGE_TYPEFIELD_NAMEARGUMENT_INDEXARGUMENT_NAMEHEADER_YN
'SERVICE_VERIFY_NAME''REQ''name'1'name'Y
'SERVICE_VERIFY_NAME''REQ''residentRegistrationNumber'2'residentRegistrationNumber'N
'SERVICE_VERIFY_NAME''RES''result'1'result'N

위 정보 중 HEADER_YN column의 값에 따라 해당 field가 Web Service Envelop의 header에 포함될지 여부를 판단한다. 위 설정값을 적용하면, 요청 메시지 중 ’name’ field는 Web Service Envelop의 헤더에 포함된다.

Server 모듈 개발

Web Service Server 모듈을 개발하는 과정은 다음과 같다.

  1. web.xml에 EgovWebServiceServlet 추가
  2. Metadata WEB_SERVICE_SERVER 설정 추가

web.xml에 EgovWebServiceServlet 추가

web.xml에 EgovWebServiceServlet 설정을 추가한다.

    ...
    <servlet>
        <description></description>
        <display-name>EgovWebServiceServlet</display-name>
        <servlet-name>EgovWebServiceServlet</servlet-name>
        <servlet-class>egovframework.rte.itl.webservice.EgovWebServiceServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>EgovWebServiceServlet</servlet-name>
        <url-pattern>/services/*</url-pattern>
    </servlet-mapping>
    ...

<url-pattern> tag의 값은 변경 될 수 있다. 자세한 설명은 다음 WEB_SERVICE_SERVER 설정을 참조한다.

Metadata WEB_SERVICE_SERVER 설정 추가

다음과 같이 Integration 서비스의 Metadata인 INTEGRATION Table에 연계등록정보가 설정되어 있다고 가정한다. (* 기관, 시스템, 서비스, 메시지타입 등의 정보는 설정되어 있으며, 공개할 서비스는 ‘SERVICE_VERIFY_NAME’이라고 가정함)

INTEGRATION
IDPROVIDER_SERVICE_KEYCONSUMER_SYSTEM_KEYDEFAULT_TIMEOUTUSING_YNVALIDATE_FROMVALIDATE_TO
'INT_VERIFY_NAME''SERVICE_VERIFY_NAME''SYSTEM_CONSUMER'5000'Y'NULLNULL

Web Service ‘SERVICE_VERIFY_NAME’를 공개하기 위해서 WEB_SERVICE_SERVER에 ‘SERVICE_VERIFY_NAME’을 SERVICE_KEY로 갖는 설정을 추가해야 한다.

WEB_SERVICE_SERVICE
SERVICE_KEYADDRESSNAMESPACESERVICE_NAMEPORT_NAMEOPERATION_NAME
'SERVICE_VERIFY_NAME''/VerifyName''http://itl/sample/''VerifyNameService''VerifyNamePort''service'

<servlet-mapping> tag의 <url-pattern> tag의 값은 서비스를 제공하기 위한 주소로, WEB_SERVICE_SERVER Table의 ADDRESS Column 값은 <url-pattern> tag값에 대한 상대 위치를 나타낸다.
예를 들어, Web Application의 IP가 192.168.0.1, Port가 8080, Context Root가 “Sample”, url-patterns이 ”/services/*”인 경우, 위 ‘SERVICE_VERIFY_NAME’의 WSDL Address는 http://192.168.0.1:8080/Sample/services/VerifyName?wsdl이다.

WAS에 배포

전자정부 WebService를 포함한 어플리케이션을 WAS에 배포(deploy)하는 방법을 설명한다. Apache CXF의 경우 Web Service 관련 라이브러리를 CXF에서 제공하는 것을 사용해야 한다. 만약 WAS가 기본적으로 Web Service 라이브러리를 제공할 경우, 정상적으로 동작하지 않을 수 있다. 따라서 CXF 라이브러리를 사용할 수 있도록 설정을 변경해야 하는데, 대부부의 해결책은 Web Application의 WEB-INF의 라이브러리를 먼저 loading하도록 Class Loading 순서를 변경하는 것이다.

본 WebService는 JAX-WS 2.0 이상을 사용한다.

TmaxSoft JEUS 6.0

전자정부 WebService의 경우 내부적으로 CXF를 사용하지만 JEUS 6.0에 배포했을 경우 Server 모듈을 공개(publish)할 때 문제가 발생한다. 그 원인은 JEUS 6.0에 기본적으로 포함되어 있는 Web Services 관련 library와 전자정부 WebService가 사용하는 library가 같지 않기 때문이다. 현재 아래와 같은 2가지 문제가 발견되었다.

  • Publish Address 문제
    Server 모듈을 publish할 때 EgovWebServiceServlet의 path에 대한 상대경로를 사용한다. Apache CXF가 사용하는 library의 경우, 이를 실제 주소로 변환해주지만, JEUS 6.0에 기본적으로 포함된 library는 그렇지 않기 때문에 IllegalArgumentException을 발생시킨다.

  • Service Endpoint Interface 참조 문제
    전자정부 WebService는 Integration 서비스 표준에 따라 Server 모듈의 Service Endpoint Interface와 구현 class를 동적으로 생성한다. 하지만 JEUS 6.0에 기본적으로 포함된 library의 경우, 이렇게 동적으로 생성된 class를 인식하지 못해서 Exception이 발생한다.

해결방법은 전자정부 WebService가 사용하는 library가 ClassLoader에서 먼저 loading되게 하는 것이다. JEUS 6.0은 jeus-web-dd.xml 설정을 통해서 WEB-INF/lib에 있는 library를 먼저 loading하도록 설정할 수 있다.

<?xml version="1.0" encoding="UTF-8"?>
<jeus-web-dd  xmlns="http://www.tmaxsoft.com/xml/ns/jeus" version="6.0">
    <webinf-first>true</webinf-first>
</jeus-web-dd>

위 jeus-web-dd.xml 파일을 web.xml 파일이 존재하는 WEB-INF 폴더에 위치시킨다. 그리고, webinf-first를 true로 설정하는 경우, XML Parser에 대한 충돌이 발생한다. 충돌을 해결하기 위해서 아래 2개의 파일을 WEB-INF/lib에서 제거해야 한다.

  • ‘xml-apis-1.0.b2.jar’ (또는 상위 버전)
  • ‘stax-api-1.0.1.jar’ (또는 상위 버전)

JBoss

JBoss의 경우, 아래 jboss-web.xml 파일을 추가한다.

<?xml version="1.0" encoding="UTF-8"?>
<jboss-web>
    <class-loading java2ClassLoadingCompliance="false">
        <loader-repository>
            apache.cxf:archive=<WAR 파일명>
            <loader-repository-config>
                java2ParentDelegation=false
            </loader-repository-config>
        </loader-repository>
    </class-loading>
</jboss-web>

*<WAR 파일명>은 deploy하는 war 파일명을 확장자를 포함하여 기재한다.

WebLogic

WebLogic 9.2 버전은 J2EE 1.4까지만 지원하므로, JAX-WS 2.0을 지원하지 않는다. WebService를 WebLogic에서 사용하기 위해서는 JAX-WS 2.0 이상을 지원하는 10.x 이상을 사용해야 한다.

참고자료

6.6 - Restful

Spring MVC를 통해 구현한 RESTful은 리소스에 대한 접근을 URI를 이용하며, HTTP의 PUT, GET, POST, DELETE 등과 같은 메소드의 의미를 그대로 사용하므로, 단순하게 접근 할 수 있다.

Restful

개요

Spring MVC를 통해 구현한 RESTful은 리소스에 대한 접근을 URI를 이용하며, HTTP의 PUT, GET, POST, DELETE 등과 같은 메소드의 의미를 그대로 사용하므로, 단순하게 접근 할 수 있다.

설명

web.xml 설정

    <servlet-mapping>
        <servlet-name>action</servlet-name>
        <url-pattern>*.html</url-pattern>
    </servlet-mapping>
        <servlet-mapping>
    <servlet-name>action</servlet-name>
        <url-pattern>*.xml</url-pattern>
        </servlet-mapping>
    <servlet-mapping>
        <servlet-name>action</servlet-name>
        <url-pattern>*.json</url-pattern>
    </servlet-mapping>
    
    <filter>
        <filter-name>httpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>httpMethodFilter</filter-name>
        <url-pattern>/springrest/*</url-pattern>
    </filter-mapping>

자세한 설명은 아래에 있다.

Request Mapping

  • 설정

    REST 스타일의 URL은 ‘/cgr’, ‘/cgr/CATEGORY-00000000001’ 처럼 계층 구조로 사용가능하도록 설계되었다. 따라서 web.xml에 DispatcherServlet을 정의하고 매핑할 URL 패턴을 ‘/‘로 지정해야한다. DispatcherServlet URL 매핑 샘플은 다음과 같다.

        <servlet-mapping>    
            <servlet-name>action</servlet-name>    
            <url-pattern>/springrest/*</url-pattern>
        </servlet-mapping>
    

    아래와 같은 방법으로도 DispatcherServlet URL 매핑을 사용 할 수 있다.

        <servlet-mapping>
            <servlet-name>action</servlet-name>
            <url-pattern>*.do</url-pattern>
        </servlet-mapping>
        <servlet-mapping>
            <servlet-name>action</servlet-name>
            <url-pattern>*.html</url-pattern>
        </servlet-mapping>
        <servlet-mapping>
            <servlet-name>action</servlet-name>
            <url-pattern>*.xml</url-pattern>
        </servlet-mapping>
        <servlet-mapping>
            <servlet-name>action</servlet-name>
            <url-pattern>*.json</url-pattern>
        </servlet-mapping>
    
  • 사용

    Spring에서 제공하는 REST 지원 기능들은 모두 Spring MVC 기반으로 되어 있다. REST 방식으로 노출되는 서비스는 곧 Controller의 메소드이기 때문에 기존에 웹 어플리케이션을 개발하던 방식과 크게 다르지 않다.

    Resource의 ID인 URI를 Controller 클래스나 메소드에 매핑하기 위해서는 @RequestMapping을 사용한다. @RequestMapping이 URI Template을 지원하기 때문에 아래 샘플코드와 같이 사용할 수 있다.

    @Controller
    @SessionAttributes(types=CategoryVO.class)
    public class EgovCategoryController {
            //…
        @RequestMapping(value="/springrest/cgr/{ctgryId}", method=RequestMethod.GET)
        public String updtCategoryView(@PathVariable String ctgryId, Model model) throws Exception{
            // …
        }
    }
    

    모든 HTTP method 사용을 위해서 @RequestMapping에서 ‘method’ 속성을 제공한다. 따라서, ‘/springrest/cgr/CATEGORY-00000000001’이라는 URI가 GET으로 요청이 들어올 경우 위의 updtCategoryView ( ) 메소드가 매핑될 것이다.

  • @PathVariable annotation추가

    ‘/springrest/cgr/CATEGORY-00000000001’로 URI요청이 들어왔을 경우 @PathVariable을 사용하여 ‘ctgryID’ 입력 인자로 바인딩 된다.

    @RequestMapping(value="/springrest/cgr/{ctgryId}", method=RequestMethod.GET)
    public String updtCategoryView(@PathVariable String ctgryId, Model model) throws Exception{
        // …
    }
    

HTTP Method Conversion

  • 설정

    브라우저 기반의 HTML에서는 GET, POST만 지원한다. 일반적으로 HTTP에서는 POST를 사용하고, hidden 타입의 입력값으로 HTTP METHOD를 지정하는 경우가 많다. 다음은 web.xml에 HiddenHttpMethodFilter를 정의한 모습이다.

        <filter>
            <filter-name>httpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
        </filter>
        <filter-mapping>
            <filter-name>httpMethodFilter</filter-name>
            <url-pattern>/springrest/*</url-pattern>
        </filter-mapping>
    
  • 사용

    web.xml에 HiddenHttpMethodFilter 설정을 추가하면, HTTP Method가 POST이고 _method라는 파라미터가 존재하는 경우 HTTP의 Method를 _method 값으로 바꾼다.

    또한 Spring에서는 <form:form>에서 실제 HTTP Method를 지정하는 hidden 타입의 입력 필드를 자동으로 추가해주기 때문에 훨씬 더 편리하게 사용할 수 있다.

        <form:form method="delete">
            <input type=submit value=Delete/>
        </form:form>
    

    JSP에 위와 같이 작성하면, 내부적으로는 POST 방식으로 “_method=delete”가 전달되는 것이다.

    샘플코드이다.

    function fncSubmit(method) {
        document.detailForm._method.value=method;
        document.detailForm.submit();
    }
    
    //..
    
    <form:form name="detailForm" method="${method}">
        <a href="javascript:fncSubmit('delete');">삭제</a>
    </form:form>
    

HTTP Method Conversion

Xml과 json 등 다른 view로 보여지는 것으로 spring에서는 ContentNegotiatingViewResolver를 제공한다. ContentNegotiatingViewResolver는 다른 View Resolver들과 반드시 함께 사용되어야 하므로 View Resolver 설정 시 반드시 order를 정의해야 한다. 당연히 ContentNegotiatingViewResolver가 가장 높은 우선순위(가장 작은숫자)를 가져야 한다. defaultView는 View를 찾지 못한 경우 디폴트 View로 사용된다.

<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
    <property name="defaultViews">
        <list>
            <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView">
                <property name="prefixJson" value="false"/>
            </bean>
        </list>
    </property>
</bean>

Views

  • MarshallingView

    클라이언트에게 xml 응답을 돌려주기 위해 Spring OXM Marshaller를 사용한다. Spring oxm는 JAXB2, XMLBeans, JiBX, Castor등을 사용하여 Marshaller를 손쉽게 정희할 수 있게 해준다. Restful 예제에서는 JAXB2를 사용하였다. (OXM예제는 Castor사용)

    <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
        <property name="mediaTypes">
            <map>
                <entry key="html" value="text/html" />
                <entry key="xml" value="application/xml" />
                <entry key="json" value="application/json" />
            </map>
        </property>
        <property name="order" value="0" />
    //..
    </beans>
    
    <bean name="cgr/egovCategoryRegister" class="org.springframework.web.servlet.view.xml.MarshallingView">
        <property name="marshaller" ref="marshaller" />
    </bean>
    
    <oxm:jaxb2-marshaller id="marshaller">
        <oxm:class-to-be-bound name="egovframework.rte.tex.cgr.service.CategoryVO" />
    </oxm:jaxb2-marshaller>
    
  • MappingJacksonJsonView

    JSON으로 응답을 전달할 수 있는 View.

    <bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
        <property name="mediaTypes">
            <map>
                <entry key="html" value="text/html" />
                <entry key="xml" value="application/xml" />
                <entry key="json" value="application/json" />
            </map>
        </property>
        <property name="order" value="0" />
        //..
    </beans>
    
    <bean name="cgr/egovCategoryList" 
    class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" />
    

실제 사용 예

  • jsp

    function fncSubmit(method) {
        document.detailForm._method.value=method;		
        document.detailForm.submit();
    }
    
    //..
    
    <form:form name="detailForm" method="${method}">
    //..
    </form:form>
    
    //..
    <a href="javascript:fncSubmit('post');">등록</a> //----------- controller 2.
    <a href="javascript:fncSubmit('put');">수정</a> //------------- controller 3.
    <a href="javascript:fncSubmit('delete');">삭제</a> //----------- controller 4.
    <a href=" /springrest/cgr/{id}.xml">xml 보기</a> // ContentNegotiatingViewResolver설정
    <a href=" /springrest/cgr/{id}.json">json(defaultView) 보기</a> // ContentNegotiatingViewResolver설정
    <a href=" /springrest/cgr.html">목록</a> //------------------ controller 1.
    <a href=" /springrest/cgr.json">목록(json)</a> // ContentNegotiatingViewResolver 설정
    
  • controller

    // 1. 목록
    @RequestMapping(value="/springrest/cgr", method=RequestMethod.GET)
    public String selectCategoryList(..) throws Exception {
        //..
    }
    
    // 2. 등록
    @RequestMapping(value="/springrest/cgr", method = RequestMethod.POST, ..)
    public String create( ..) throws Exception {
        //..
    }
    
    // 3. 수정
    @RequestMapping(value = "/springrest/cgr/{ctgryId}", method = RequestMethod.PUT, ..)
    public String update(..) throws Exception {
        //..
    }
    
    // 4. 삭제
    @RequestMapping(value = "/springrest/cgr/{ctgryId}", method=RequestMethod.DELETE)
    public String deleteCategory(@PathVariable String ctgryId, SessionStatus status) throws Exception{
        //..
    }
    

참고자료

6.7 - Cloud Data Stream

Spring Cloud Stream은 확장 가능한 이벤트 기반 마이크로서비스를 구축하기 위한 프레임워크로, 외부 메시징 시스템과 애플리케이션 코드를 연결하는 바인더 및 바인딩 기능을 제공한다. 생산자와 소비자는 메시지를 통해 통신하며, Spring Integration의 메시지 처리 기능을 활용한다. Spring Boot 기반의 Binder 구현체를 통해 이기종 시스템 간에도 메시지 처리가 가능하다.

Cloud Data Stream

개요

Spring Cloud Stream은 공유 메시징 시스템과 연결된 확장성이 뛰어난 이벤트 기반 마이크로서비스를 구축하기 위한 프레임워크이다.

Spring Cloud Stream의 핵심 구성 요소는 다음과 같다.

  • 대상 바인더 : 외부 메시징 시스템과의 통합을 담당하는 구성 요소이다.
  • 대상 바인딩 : 외부 메시징 시스템과 최종 사용자가 제공하는 애플리케이션 코드(생산자/소비자) 사이를 연결한다.
  • 메시지 : 생산자와 소비자가 대상 바인더(및 외부 메시징 시스템을 통한 다른 응용 프로그램)와 통신하는 데 사용하는 표준 데이터 구조이다.

Spring Cloud Stream은 Spring Integration의 메시지 처리 핵심 기능을 기반으로 사용한다.
또한 Spring Boot를 기반으로 Binder 구현체를 제공하여 메시지 처리를 추상화 하여 동일 환경 뿐만 아니라 이기종의 시스템 또는 다른 환경 간에도 연계 메시지 처리를 지원한다.

특징

Data Stream

비동기 데이터 처리는 지속적으로 발생하는 데이터에 대하여 실시간으로 처리 하는데 주요 목적이 있으며, 시간에 비교적 민감한 자료의 처리에 적합하며 다양한 지리적 위치에서 다양한 형식으로 전달될 수 있다.

Data Stream , Batch 비교

Data Stream vs Batch

배치 처리비동기 데이터 처리
한정된 대량의 데이터지속적으로 데이터가 발생
스케줄러를 사용하여 특정 시간에 처리데이터 발생주기는 일정한 경우와 불규칙한 경우 모두 가능
일괄로 정해진 묶음단위 처리데이터를 실시간으로 처리

설명

예제 코드

java.util.function 패키지의 Functional Interface를 기반으로 람다식 사용 시 Supplier, Function, Consumer를 활용하여 클래스를 생성하지 않고 구현이 가능하다.
이 경우 Supplier는 Sink Binding으로 1초마다 주기적으로 발행된다.

@Slf4j
@Configuration
public class DataStreamConfig {
 
    @Bean
    public Supplier<String> basicProducer() {
    	return () -> "Hello";
    }
 
    @Bean
    public Function<String, String> uppercase() {
        return value -> value.toUpperCase();
    }
 
    @Bean
    pubilc Consumer<String> basicConsumer() {
    	return message -> log.info("message = {}", message);
    }
}

StreamBridge

불규칙하게 발행되는 경우 StreamBridge를 활용 할수 있다.

    private void processChangeHistory(long elapsedTimeMills, String className, String methodName, Sample content) {
        SampleDTO sampleDTO = new SampleDTO();
        sampleDTO.setCategory("change");
        sampleDTO.setContent(content);
        sampleDTO.setClassName(className);
        sampleDTO.setMethodName(methodName);
        sampleDTO.setElapsedMills(elapsedTimeMills);
 
        streamBridge.send("historyDb", sampleDTO);
    }

Deprecated 내용

Spring Cloud Stream v3.x에서 org.springframework.cloud.stream.annotation 패키지에 포함된 대부분의 어노테이션이 Depreaceted 되었다.
따라서 v3.x이상에서는 함수형 프로그래밍 방식으로 작성 및 설정 해야 한다.

@EnableBinding
@StreamListener
@Input
@Output
@StreamMessageConverter

설정방법

pom.xml 설정

대표적으로 RabbitMQ Binder 및 Kafka Binder를 지원하며 그외에도 다양한 바인더를 지원한다.

	<!-- Spring Cloud Stream RabbitMQ Binder -->
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
		<version>3.2.4</version>
	</dependency>
 
	<!-- Spring Cloud Stream Kafka Binder -->
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-stream-binder-kafka</artifactId>
		<version>3.2.4</version>
	</dependency>

application.yml 설정

RabbitMQ 연결 설정 예시

spring:
  rabbitmq:
    host: 192.168.100.50
    port: 5672
    username: guest
    password: guest
    virtual-host: egov

Kafka 연결 설정 예시

spring:
  cloud:
    stream:
      kafka:
        binder:
          autoCreateTopics: false
          autoAddPartitions: false
          zkNodes: 192.168.100.50
          brokers: 192.168.100.50

바인딩 설정

단일 바인딩의 경우 다음과 같이 간단하게 설정 가능 하다.

spring:
  cloud:
    stream:
      bindings:
        output:
          destination: sample-topic
        input:
          destination: sample-topic

멀티 바인딩 설정

다음 네이밍 컨벤션을 반드시 따라야 한다.
input : {functionName} + -in- + {index}
output : {functionName} + -out- + {index}

spring:
  cloud:
    stream:
      bindings:
        basicProducer-out-0:
          destination: test-topic
          binder: kafka
        basicConsumer-in-0:
          destination: test-topic
          binder: rabbit
      function:
        definition: basicProducer;basicConsumer;

참고자료

6.8 - Swagger

Swagger는 Restful 서비스의 문서화를 자동으로 지원하는 도구로, API 서버의 스펙과 주고받는 데이터를 명확하게 문서화할 수 있다. 수동으로 문서를 작성하고 유지보수하는 데 드는 시간과 비용을 줄여주며, API 스펙 변경 시 문서도 자동으로 업데이트된다. 이를 통해 Restful 서비스의 문서 작성과 유지보수를 효율적으로 관리할 수 있다.

Swagger

개요

Swagger는 Restful 서비스 사용시 구현된 서비스에 대한 문서화를 지원하는 도구이다.

설명

목적

Restful 서비스를 구현한 경우 해당 API서버가 어떤 스펙을 가지고 있고 어떤 데이터를 주고 받는지에 대한 문서작업은 꼭 필요하다.
하지만 이런 문서작업은 상당한 시간을 사용하여 작성하여야 하고 API서버의 스펙이 변경되면 문서도 수정해 주어야 하기 때문에 관리가 여간 어려운게 아니다.
따라서 API 서버의 서비스를 작성하는것외에 문서의 작성과 유지보수를 위해 많은 시간과 비용이 발생한다.
Swagger는 이러한 Restful서비스의 문서작성과 유지보수에 대한 효율성을 높일수 있다.

특징

간단한 설정으로 Swagger UI를 구동시킬수 있다.

Swagger Intro01

그룹별로 정리할수 있으며 간단한 정보를 안내할수 있다.

그룹별로 정리되기 위해서는 URL경로가 업무별로 구분가능하고 정리되어 있어야 한다.
해당 서비스에 대해 기본적인 정보를 안내할수 있다.

Swagger Intro02

UI에서 전문의 각항목의 정의명을 표시해 줄수 있다.

Swagger Definition

UI에서 테스트를 수행할수 있다. 각 항목의 입력은 물론 파일 업로드까지 테스트가 가능하다.

Swagger Test01

테스트 결과가 UI에서 즉시 표시된다.

Swagger Test02

설정방법

pom.xml 설정

	<dependency>
	    <groupId>io.springfox</groupId>
	    <artifactId>springfox-swagger2</artifactId>
	    <version>2.9.2</version> 
	</dependency>
 
	<dependency>
	    <groupId>io.springfox</groupId>
	    <artifactId>springfox-swagger-ui</artifactId>
	    <version>2.9.2</version>
	</dependency>

swagger-servlet.xml 설정

    <context:component-scan base-package="egovframework">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
    </context:component-scan>
 
	<bean id="swagger2Config"
		class="springfox.documentation.swagger2.configuration.Swagger2DocumentationConfiguration"></bean>
 
	<mvc:resources location="classpath:/META-INF/resources/"
		mapping="swagger-ui.html"></mvc:resources>
	<mvc:resources
		location="classpath:/META-INF/resources/webjars/"
		mapping="/webjars/**"></mvc:resources>

web.xml 설정

  <servlet>
    <servlet-name>swagger</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/config/egovframework/springmvc/swagger-servlet.xml</param-value>
    </init-param>
    <load-on-startup>2</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>swagger</servlet-name>
    <url-pattern>/swagger-ui.html</url-pattern>
    <url-pattern>/webjars/**</url-pattern>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

SwaggerConfig 작성

@EnableSwagger2 어노테이션을 반드시 추가하여야 한다.

@Configuration
@EnableSwagger2
@EnableWebMvc
public class SwaggerConfig {
 
    @Bean
    public Docket newsApiAll() {
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("00. All Device API REST Service")
                .apiInfo(apiInfo())
                .select()
                .paths(PathSelectors.any())
                .build();
    }
 
    @Bean
    public Docket newsApiAccelerator() {
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("01. Accelerator Guide Program Service")
                .apiInfo(apiInfo())
                .select()
                .paths(regex("/acl.*"))
                .build();
    }
 
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("표준프레임워크 DeviceAPI 연계서비스 (Hybrid App)")
                .description("표준프레임워크 하이브리드앱 실행환경  - iOS / Android 하이브리드앱 Rest 서비스")
                .termsOfServiceUrl("https://www.egovframe.go.kr/wiki/doku.php?id=egovframework:hyb:gate_page")
                .license("Apache License Version 2.0")
                .licenseUrl("https://www.egovframe.go.kr")
                .version("3.10")
                .build();
    }
}

7 - 배치처리

배치처리 서비스는 일괄처리 업무 구현에 필요한 기능을 제공한다.

배치 실행환경

배치처리 서비스는 일괄처리 업무 구현에 필요한 기능을 제공한다.

7.1 - 배치 실행환경 소개

전자정부 표준프레임워크는 대용량 데이터 처리 지원을 위해 작업 수행, 결과 관리, 스케줄링 관리 기능을 제공한다.

배치 실행환경 소개

개요

전자정부 표준프레임워크에서 대용량 데이터 처리 지원을 위해 작업 수행, 결과 관리, 스케줄링 관리 기능을 제공한다.

목표

배치 실행환경은 대용량 데이터 처리를 위한 기반 환경을 제공함으로써 배치 실행에 필요한 핵심 기능을 제공한다.

배치 실행환경 수행 과정

전자정부 표준프레임워크 실행환경에 추가된 배치 실행환경은 3-Tier(Run, Job, Application Tier)로 구성되며, 대용량 데이터 처리를 위한 기반 환경을 제공한다.

배치 실행환경 수행 과정

Run Tier

Run Tier는 배치 응용 프로그램의 실행을 담당한다. 실행 방식에 따라 Scheduler, Http/Web service, CommandLine으로 나눌 수 있다.

✔ Spring 배치에서는 Scheduler 실행을 위해 QuartzCron을 이용하도록 권고하고 있다.

Run Tier에서의 동작 순서는 다음과 같다:

  1. Job Configuration은 XML 형태로, Job을 수행하는 데 필요한 설정 정보를 담고 있다.
  2. Scheduler, Http/Web service, CommandLine 등의 외부 모듈이 JobRunner를 호출한다.
  3. JobRunnerJobLocator(JobExplorer)를 통해 Job Configuration에 등록된 Application Context 정의를 참조하여, Job TierJobLauncher가 Job을 실행할 수 있도록 정보를 전달한다.

Job Tier

Job Tier는 전체적인 Job 수행을 책임지며, 각 Step을 지정된 상태와 정책에 따라 순차적으로 수행한다.

Job Tier에서의 동작 순서는 다음과 같다:

  1. eGovJobeGovStep은 각각 Spring 배치의 Job과 Step을 참조한 것으로, XML 형태로 작성되어 있다.
  2. JobLauncherJobRunner로부터 전달받은 Job 설정 정보와 정의된 내용을 바탕으로 실제 Job을 수행한다.
  3. JobRepository는 수행되는 Job의 정보를 담고 있으며, Job 수행 단계에 따라 상태 정보를 저장한다.

Application Tier

Application Tier는 Job과 Step을 수행하는 데 필요한 컴포넌트로 구성된다.

Application Tier에서의 동작 순서는 다음과 같다:

  1. eGovStep은 Spring 배치의 Step을 참조한 것으로, XML 형태로 기술되어 있다. 하나의 Job은 하나 혹은 여러 Step으로 구성된다.
  2. eGovStepReader / eGovStepWriter는 Spring Batch의 ItemReader / ItemWriter를 참조한 것으로, 일반적인 Step 동작에 필수적이다.
  3. Step은 ItemReader를 이용해 File/DB에서 데이터를 읽고, ItemProcessor로 데이터를 가공한 후, ItemWriter를 통해 가공된 데이터를 다시 File/DB로 쓴다.

배치 실행환경 기술 요소 구성

전자정부 표준프레임워크 실행환경에 포함된 대용량 데이터 처리 계층은 Job 구조를 정의하는 Batch Core, Job 실행을 지원하는 Batch Support, 다양한 실행환경을 지원하는 Batch Execution으로 구성되어 있다. 배치 실행환경의 기술 요소와 기능은 다음 그림과 같다.

배치 실행환경 기술요소 구성


배치 실행환경 지원

  • SQLite ↑
    SQLite를 사용한 경량화된 Repository 사용법을 설명한다.

  • Logback logging ↑
    SQLite를 사용한 경량화된 로깅 처리의 기본 사용법을 설명한다.

7.2 - SQLite

SQLite를 이용한 경량화된 Repository를 사용하기 위한 사용법에 대해 설명한다.

SQLite

개요

배치 처리시 경량화된 Repository를 사용을 위한 SQLite 처리를 지원한다.

설명

SQLite pom.xml 설정

sqlite 라이브러리 사용을 위해 dependency를 추가 한다.

<dependency>
	<groupId>org.xerial</groupId>
	<artifactId>sqlite-jdbc</artifactId>
	<version>x.x.x</version>
</dependency>

SQLite 사용

SQLite 사용을 위해 데이터베이스 설정을 하고 repository 생성을 위한 기초데이터를 설정 한다.

<!-- SQLite database  설정 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
	<property name="driverClassName" value="org.sqlite.JDBC" />
	<property name="url" value="jdbc:sqlite:repository.sqlite" />
	<property name="username" value="" />
	<property name="password" value="" />
</bean>

<!-- SQLite 기초데이터 설정 -->
<jdbc:initialize-database data-source="dataSource">
	<jdbc:script location="org/springframework/batch/core/schema-drop-sqlite.sql" />
	<jdbc:script location="org/springframework/batch/core/schema-sqlite.sql" />
 </jdbc:initialize-database>

7.3 - Logback logging

SQLite를 이용하여 경량화된 로깅 처리를 하는 기본 사용법에 대해 설명한다.

Logback logging

개요

배치 처리시 로깅 처리를 위해 log4j2를 지원하고 있지만 경량화된 로깅 처리를 위해 Logback 로깅 처리를 지원한다

설명

Logback pom.xml 설정

log4j, commons-logging 관련 라이브러리를 exclusion 처리하고, Logback 라이브러리를 등록한다.

<!-- log4j 관련 exclusion -->
<dependency>
	<groupId>egovframework.rte</groupId>
	<artifactId>egovframework.rte.bat.core</artifactId>
	<version>${egovframework.rte.version}</version>
	<exclusions>
		<exclusion>
			<artifactId>log4j-core</artifactId>
			<groupId>org.apache.logging.log4j</groupId>
		</exclusion>
		<exclusion>
			<artifactId>log4j-slf4j-impl</artifactId>
			<groupId>org.apache.logging.log4j</groupId>
		</exclusion>
		<exclusion>
			<artifactId>log4j-over-slf4j</artifactId>
			<groupId>org.slf4j</groupId>
		</exclusion>
		<exclusion>
			<artifactId>commons-logging</artifactId>
			<groupId>commons-logging</groupId>
		</exclusion>
	</exclusions>
</dependency>
 
<!-- commons-logging 관련 exclusion -->
<exclusion>
	<artifactId>commons-logging</artifactId>
	<groupId>commons-logging</groupId>
</exclusion>
 
<!-- logback 관련 라이브러리 등록 -->
<dependency>
	<groupId>ch.qos.logback</groupId>
	<artifactId>logback-core</artifactId>
	<version>1.1.7</version>
</dependency>
<dependency>
	<groupId>ch.qos.logback</groupId>
	<artifactId>logback-classic</artifactId>
	<version>1.1.7</version>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>jcl-over-slf4j</artifactId>
  <version>1.7.21</version>
</dependency>

SQLite 사용

logback 사용을 위해 logback.xml를 설정이 선행 되어야 한다. 설정관련 자세한 사항을 아래 링크 참고

<!-- 설정 예시 -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<encoder>
			<pattern>[logback]%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
		</encoder>
	</appender>
	<logger name="java.sql" level="DEBUG" />
	<logger name="egovframework" level="DEBUG" />
	<logger name="jdbc.sqltiming" level="DEBUG" />
	<logger name="org.springframework" level="DEBUG" />
	<root level="DEBUG">
		<appender-ref ref="STDOUT" />
	</root>
</configuration>

참고자료

7.4 - Job

Job은 배치 작업 전체를 캡슐화하는 개념으로, 여러 Step을 포함하며 배치 작업 실행을 의미한다. Job은 각 JobParameters에 따라 JobInstance를 생성하고, Job 시도마다 JobExecution을 생성하여 작업을 처리한다.

Job

개요

Job은 배치작업 전체의 중심 개념으로 배치작업 자체를 의미한다. Job은 실제 프로세스가 진행되는 Step 들을 최상단에서 포함하고 있으며, Job의 실행은 배치작업 전체의 실행을 의미한다.

설명

  • Job은 배치작업 과정 전체를 캡슐화하는 개념이며, 전체 계층 구조의 최상단이다.
  • 특정 Job은 각각의 JobParameters에 따라 JobInstance를 생성하며, 한번의 Job 시도마다 JobExecution을 생성한다.
  • Job은 반드시 한개 이상의 Step으로 구성된다.

아래 그림을 보면, ‘EndOfDay’라는 Job이 있고 ‘2012/10/01’이라는 JobParameter를 통해 JobInstance가 생성되었다. 그리고 ‘EndOfDay’ Job의 첫번째 시도를 의미하는 JobExecution이 생성되는 것을 볼 수 있다.

job-structure

Job 인터페이스의 기본적인 구현은 SimpleJob 클래스로 스프링 배치에서 제공된다. SimpleJob 클래스는 모든 Job에서 유용하게 사용할 수 있는 표준 기능을 갖고있다. Job은 아래와 같이 <job> 태그를 사용하여 설정할 수 있다.

<job id="footballJob">
   <step id="playerload" next="gameLoad"/>
   <step id="gameLoad" next="playerSummarization"/>
   <step id="playerSummarization"/>
</job>

JobInstance

JobInstance는 논리적 Job 실행의 개념으로 JobInstance = Job + JobParameters로 표현할 수 있다. 다시 말해, JobInstance는 동일한 Job이 각기 다른 JobParameter를 통해 실행 된 Job의 실행 단위이다. (Job과 JobParameters가 같으면 동일한 JobInstance이다

jobinstance-jobparameter-description

위의 그림을 예로 설명하면 매일 한번씩 실행되는 ‘EndOfDay’라는 Job이 있다고 가정한다. ‘EndOfDay’라는 Job은 하나지만 매일 실행되는 각각의 ‘EndOfDay’ Job은 구별되어야 한다. ‘2012/10/01’에 실행 된 ‘EndOfDay’ Job과 ‘2012/10/02’에 실행 된 ‘EndOfDay’ Job은 같은 Job이지만 JobInstance가 다르다. 이런 특성을 이용해 JobInstance는 Job의 Restart에 이용할 수 있다. JobInstance를 Restart하는 것은 해당 JobInstance의 정보(Execution Context)를 재사용하는 것이므로 새로운 JobInstance를 생성하지 않는다.(새로운 JobExecution이 생성된다.)

아래 표는 ‘EndOfDay’라는 하나의 Job이 JobParameter로 구별되어 각기 다른 JobInstance를 생성할 수 있음을 보여준다.

JobInstance IDJob NameJobParameters
1EndOfDay2012/10/01
2EndOfDay2012/10/02

JobParameters

JobParameters는 하나의 Job에 존재할 수 있는 여러개의 JobInstance를 구별하기 위한 Parameter 집합이며, Job을 시작하는데 사용하는 Parameter 집합이다. 또한 Job이 실행되는 동안에 Job을 식별하거나 Job에서 참조하는 데이터로 사용된다. 위의 그림(JobInstance 부분)으로 예를들면 ‘EndOfDay’ Job으로 2개의 JobInstance가 생성됐다. 이 2개의 JobInstance는 각기 다른 JobParameters(‘2012/10/01’, ‘2012/10/02’)를 통해 생성된 것이다. 아래 표에서 JobInstance는 각각의 JobParameters를 갖고 있음을 볼 수 있다.

JobInstance IDJobParametersJob Name
12012/10/01EndOfDay
22012/10/02EndOfDay

JobParameter 구조

  • JobParameter클래스에는 실제 parameter의 내용과 ParameterType이 존재한다.
public class JobParameter implements Serializable {
 
	private final Object parameter;
 
	private final ParameterType parameterType;
 
}
  • ParameterType은 enum 형태로 String, Date, Long, Double을 받아들일 수 있다.
public enum ParameterType {
 
	STRING, DATE, LONG, DOUBLE;
}
  • JobParameters는 Map 형태로 관리된다.
public class JobParameters implements Serializable {
 
	private final Map<String,JobParameter> parameters;
 
	public JobParameters() {
		this.parameters = new LinkedHashMap<String, JobParameter>();
	}
 
	public JobParameters(Map<String,JobParameter> parameters) {
        	this.parameters = new LinkedHashMap<String,JobParameter>(parameters);
	}

JobParameter 생성 방법

  • JobParameterBuilder 클래스 사용
protected JobParameters getUniqueJobParameters() {
		return new JobParametersBuilder(super.getUniqueJobParameters())
		.addString("inputFile","data/iosample/input/delimited.csv")
		.addString("outputFile","file:./target/test-outputs/delimitedOutput.csv").toJobParameters();
}
  • DefaultJobParametersConverter 클래스 사용
new DefaultJobParametersConverter()
		.getJobParameters(PropertiesConverter
	.stringToProperties("run.id(long)=1,parameter=true,run.date=20121001"));

생성된 JobParameters는 XML에서 사용할 수 있다.

  • ex) FlatFileItemReader
    resource에 JobParameter에 ‘inputFile’이라는 Parameter 이름(Key 값)을 통해 입력 받을 수 있다.
<bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader" >
	<property name="resource" value="#{jobParameters[inputFile]}" />
                    (중략...)
</bean>
  • ex) FlatFileItemWriter
    resource에 JobParameter에 ‘outputFile’이라는 Parameter 이름(Key 값)을 통해 입력 받을 수 있다.
<bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemWriter" >
	<property name="resource" value="#{jobParameters[outputFile]}" />
                    (중략...)
</bean>

JobExecution

JobExecution은 한번의 Job 시도를 의미하는 기술적인 개념이다. JobExecution은 ‘FAILED’ 또는 ‘COMPLETED’로 Job의 시도 결과를 나타낸다. 이외에, JobExecution은 주로 Job이 실행 중에 어떤 일이 일어났는지에 대한 속성들을 저장하는 저장 메커니즘 역할을 한다. (JobExecution 속성 자세히 보기)
아래의 그림을 예로들면, ‘EndOfDay’ Job은 2개의 JobInstance를 갖고 3개의 JobExecution이 존재하는 것을 볼 수 있다. JobInstance가 매일 한번 실행되는 Job을 구분하는 논리적인 개념이라면, JobExecution은 3번의 Job 시도 자체를 의미한다. ‘2012/10/02’라는 JobParameter로 실행된 ‘EndOfDay’ Job은 ID가 2인 JobInstance를 생성하게 된다. 첫번째 Job의 시도는 FAILED로 끝나게 됐고 두번째 시도는 COMPLETED로 완료하게 된다. 즉, ‘2012/10/02’에 ‘EndOfDay’ Job은 총 2번의 Job 시도로 2개의 JobExecution이 생성됐다. ✔ Status가 COMPLETED 인 JobExecution을 가진 JobInstance는 restart를 할 수 없다.(해당 JobInstance는 정상적으로 배치작업을 완료)

jobexecution-description

아래의 표를 보면 ‘EndOfDay’ Job이 각각 다른 ‘2012/10/01’, ‘2012/10/02’ JobParameter로 두 번 실행결과 JobInstance는 2개가 생성 됐고, Job의 3번 시도에 따라 3개의 JobExecution이 생성된 것을 볼 수 있다. 여기서 중요한 점은 JobExecution은 매 시도마다 새로 생성되지만 JobInstance가 같은 JobExecution은 동일한 JobParameter로 시도 됐다는 점이다.

JobExecution IDJobInstance IDStart TimeEnd TimeStatus
112012.10.01.12:002012.10.01.12:10COMPLETED
222012.10.02.12:002012.10.02.12:10FAILED
322012.10.02.12:302012.10.02.12:40COMPLETED

Job Configuration

일반적인 스프링 프로젝트에서 Job은 XML 설정 파일을 통해 표현되며, 의존성을 맺게 되고 이 설정 파일은 “Job 설정”이라 한다.

Job 필수 설정

<job id="footballJob" job-repository="specialRepository">
	<step id="playerload" parent="s1" next="gameLoad"/>
	<step id="gameLoad" parent="s3" next="playerSummarization"/>
	<step id="playerSummarization" parent="s3"/>
</job>
  • ID : Job의 식별자
  • <step> : Job은 적어도 하나 이상의 Step을 정의해야 한다.
  • job-repository : 배치작업 중 JobExecution을 주기적으로 저장하기 위한 저장소 (default 설정은 ‘jobRepository’로 생략 가능)

상속을 이용한 설정

parent

‘parent’ 속성은 Job을 상속하여 유사한 설정의 Job이 여러 개일 경우 유용하게 사용할 수 있다. Java에서 클래스 상속과 유사하게 자식 Job은 부모 Job의 속성들과 자신의 속성들을 결합한다. 또한 부모 Job의 속성을 오버라이드 하여 사용할 수도 있다.

abstract

Java의 Abstract 클래스와 동일한 개념으로 때로는 완전한 Job을 구성하지 않는 부모 Job의 정의가 필요할 때가 있다. ‘abstract’ 속성은 Job 설정이 추상레벨인지 여부를 지정한다.

아래 예제에서 “baseJob”은 abstract 선언되었으며 자식 Job인 “job1”에서는 Job 설정의 필수요소인 ‘<step>‘을 정의해야 하며 ‘Job1’은 “baseJob”의 “listenerOne”도 상속받아 설정된다.

<job id="baseJob" abstract="true">
	<listeners>
		<listener ref="listenerOne"/>
	<listeners>
</job>
 
<job id="job1" parent="baseJob">
	<step id="step1" parent="standaloneStep"/>
	<listeners merge="true">
		<listener ref="listenerTwo"/>
	<listeners>
</job>

Restart 가능 여부 설정

restartable

Job은 완료되지 않은 JobInstance를 재시작할 수 있다. Job 설정 시, 해당 Job에 만들어진 JobInstance들의 재시작 가능 여부를 ‘restartable’ 속성을 통해 설정할 수 있다

<job id="footballJob" restartable="false">
   ...
</job>

Job Variable

변수 선언 후 Listeners를 통해서 모든 Job에서 사용자 정의 변수를 사용할 수 있도록 EgovJobVariableListener를 통해서 지원한다.

job Variable 설정 자세히 보기

참고자료

7.5 - Job Variable

EgovJobVariableListener는 사용자 정의 변수를 선언하고 여러 Job에서 이를 공유하여 사용할 수 있도록 지원하는 기능을 제공한다. 이를 통해 모든 Job에서 공통 변수를 활용할 수 있다.

Job Variable

개요

변수 선언 후 Job Listeners를 통해서 모든 Job에서 사용자 정의 변수를 사용할 수 있도록 EgovJobVariableListener를 통해서 지원한다.
사용자가 변수를 정의하여 여러 job에서 해당 변수를 공유하여 사용 가능한 기능으로 이루어져있다.

job_variable_architecture6

설명

Job Variable 설정

배치실행환경에서 제공하는 EgovJobVariableListener 사용하여 사용자 정의 변수를 설정한다.

<bean id="egovJobVariableListener" class="egovframework.rte.bat.support.EgovJobVariableListener">
<property name="pros">
<props>
	<prop key="JobVariableKey1">JobVariableValue1</prop>
	<prop key="JobVariableKey2">JobVariableValue2</prop>
	<prop key="JobVariableKey3">JobVariableValue3</prop>
</props>
</property>
</bean>

job 설정

job 설정시 listener를 사용하여 공유변수 서비스를 설정한다.

<job id="delimitedToDelimitedJob-JobVariable" parent="eGovBaseJob" xmlns="http://www.springframework.org/schema/batch">
	<listeners>
		<listener ref="egovJobVariableListener" />
	</listeners>
	<step id="step1">
		<tasklet ref="taskletJob" />
	</step>
</job>

job에서 tasklet 선언시 Step에서 Job Variable 사용

setter 방식으로 공유변수 사용시 아래와 같이 응용하여 설정한다.

<bean id="taskletJob" class="egovframework.example.bat.step.TaskletJob" scope="step">
	<property name="jobVariable" value="#{jobExecutionContext[JobVariableKey1]}" />
</bean>

Setp 처리시 Job Variable 사용

public class TaskletJob implements Tasklet, InitializingBean {
 
	private String jobVariable;
 
	@Value("#{jobExecutionContext[JobVariableKey2]}")
	private String vJobVariable;
 
	public String getJobVariable() {
		return jobVariable;
	}
 
	public void setJobVariable(String jobVariable) {
		this.jobVariable = jobVariable;
	}
 
	@Override
	public RepeatStatus execute(StepContribution contribution,
	               ChunkContext chunkContext) throws Exception {
 
		//Tasklelt 선언시 setter의해 선언된  Job Variable : jobVariable
 
		//annotation @Value 통해 선언된  Job Variable : vJobVariable
 
		//direct 접근을 통한 Job Variable 사용 : chunkContext.getStepContext().getJobExecutionContext().get("JobVariableKey3")
		return RepeatStatus.FINISHED;
	}
}

7.6 - Step

Step은 Job 내에서 배치 작업을 정의하고 제어하는 독립적이고 순차적인 단계를 캡슐화하는 도메인 객체이다. 모든 Job은 최소 하나 이상의 Step으로 구성되며, 각 Step은 입력, 처리, 출력 자원 설정을 포함하여 작업을 처리한다. StepExecution은 JobExecution과 대응되며, 각 Step은 순차적으로 실행된다.

Step

개요

Step은 Job 내부에 구성되어 실제 배치작업 수행을 위해 작업을 정의하고 제어한다. 즉, Step에서는 입력 자원을 설정하고 어떤 방법으로 어떤 과정을 통해 처리할지 그리고 어떻게 출력 자원을 만들 것인지에 대한 모든 설정을 포함한다.

설명

Step은 Job의 독립적이고 순차적 단계를 캡슐화하는 도메인 객체다. 그러므로 모든 Job은 적어도 하나 이상의 Step으로 구성되며 Step에 실제 배치작업을 처리하고 제어하기 위해 필요한 모든 정보가 포함된다. 여러개의 Step 중 하나의 Step은 순차적으로 실행되는 과정 중 하나의 흐름으로 생각할 수 있다. Step에는 JobExecution에 대응되는 StepExecution이 있다.

step-structure

Step 유형

Bean Scope

스프링 빈 선언시 Bean Scope 기본 전략을 singleton를 사용하고 있지만, 스프링 배치에서는 step에서 Bean Scope에 대해 job, step 설정이 가능하다.

Step Scope

scope step : 하나의 빈 정의에 대해 step 안에서 lifecycle이 유효하다.

<bean id="..." class="..." scope="step">
Job Scope

scope job : 하나의 빈 정의에 대해 job 안에서 lifecycle이 유효하다.

<bean id="..." class="..." scope="job">

Chunk 기반 처리(Chunk-Oriented Processing)

Chunk 기반 처리는 스프링 배치에서 가장 일반적으로 사용하는 Step 유형이다. Chunk 기반 처리는 data를 한번에 하나씩 읽고, 트랜잭션 범위 내에서 ‘Chunk’를 만든 후 한번에 쓰는 방식이다. 즉, 하나의 item이 ItemReader를 통해 읽히고, Chunk 단위로 묶인 item들이 한번에 ItemWriter로 전달 되어 쓰이게 된다.

Chunk 단위로 Item 읽기 → 처리/변환 → 쓰기의 단계를 거치는 Chunk 기반 처리 매커니즘은 다음과 같다

chunk-oriendted-step

아래 코드는 위의 그림과 같은 개념의 코드이다.

List items = new Arraylist();
for(int i = 0; i < commitInterval; i++){
    Object item = itemReader.read()

    Object processedItem = itemProcessor.process(item);

    items.add(processedItem);
}

itemWriter.write(items);
  • 구성요소: ItemReader, ItemWriter, PlatformTransactionManager, JobRepository, (ItemProcessor는 옵션)
  • 중요속성: commit-interval(하나의 트랜잭션당 처리 개수), startLimit(step의 실행 제한 횟수)

TaskletStep

배치작업을 적용한 업무 환경에 따라 ItemReader와 ItemWriter를 활용한 구조가 맞지 않는 경우도 있을 것이다. 예를들어 단순히 DB의 프로시저 호출만으로 끝나는 배치처리가 있다면 단순히 메소드 하나로 기능을 구현하고 싶어질 것이다. 이런 경우를 위해 스프링 배치에서는 TaskletStep을 제공한다. Tasklet은 RepeatStatus.FINISHED를 반환하거나 에러가 발생하기 전까지 계속 실행하는 execute() 하나의 메소드를 갖는 간단한 인터페이스로 저장 프로시저, 스크립트, 또는 간단한 SQL 업데이트 문을 호출 할 수 있다.

TaskletStep을 구성하기 위해서는 <tasklet> 태그의 ‘ref’속성을 통해 Tasklet 객체를 참조해야한다. <chunk> 태그는 <tasklet> 내에서 사용되지 않는다.(<Chunk> 태그는 Chunk-Oriented Processing에서 사용된다.)

<step id="step1">
   <tasklet ref="myTasklet"/>
</step>
Tasklet 의 execute() 메소드를 Implement해서 구현

Tasklet 인터페이스를 구현한 SystemCommandTasklet 클래스를 이용해서 “echo hello"라는 명령어를 5초 동안의 timeout시간을 두고 실행시키는 설정의 예

<bean id="myTasklet">
    <property name="tasklet">
        <bean class="org.springframework.batch.sample.tasklet.SystemCommandTasklet">
              <property name="command" value="echo hello" />
              <property name="timeout" value="5000" />
        </bean>
    </property>
</bean>
관련 예제

단순처리(Tasklet) 예제

StepExecution

Job의 JobExecution과 대응되는 단위로 Step 또한 StepExecution을 갖고 있다. JobExecution과 마찬가지로 StepExecution은 Step을 수행하기 위한 단 한번의 Step 시도를 의미하며 매번 시도될 떄마다 생성된다. 또한, StepExecution은 주로 Step이 실행 중에 어떤 일이 일어났는지에 대한 속성들을 저장하는 저장 메커니즘 역할을 하며 commit count, rollback count, start time, end time 등의 Step 상태정보를 저장한다. (StepExecution 속성 자세히 보기)

아래의 그림에서 ‘Step1’, ‘Step2’ 2개의 Step을 갖는 ‘EndOfDay’ Job이 두번 실행되었다고 가정하자.(두번 시도 결과 JobExecution은 2개 생성) ‘EndOfDay’ Job을 시도할 때마다 ‘Step1’, ‘Step2’도 시도 되기때문에 StepExecution은 2개 씩 생성된다. 그래서 총 4개의 StepExecution이 생성된 것을 볼 수 있다.

✔ StepExecution 4번이 FAILED로 종료 됐으므로 StepExecution 3번, 4번을 포함한 JobExecution 2번은 FAILED로 종료한다.(Step이 모두 정상적으로 완료해야 Step으로 구성된 Job이 정상적으로 완료된다.)

stepexecution-description

위의 그림을 정리해보면 아래와 같다.

StepExecution ID Step Name JobExecution ID Status

StepExecution IDStep NameJobExecution IDStatus
1Step11COMPLETED
2Step21COMPLETED
3Step12COMPLETED
4Step22FAILED

Step Configuration

Step 구성은 개발자에 따라 간단하거나 아주 복잡하게 구성할 수 있다. 다만 구성을 쉽게하기 위해 스프링 배치 네임스페이스를 사용할 수 있다.

Chunk 기반 Step 필수 설정

<job id="sampleJob" job-repository="jobRepository">
	<step id="step1">
		<tasklet transaction-manager="transactionManager">
			<chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
		<tasklet>
	</step>
</job>
  • reader : 배치작업을 위해 item을 읽는 ItemReader
  • writer : ItemReader에 의해 읽힌 item을 쓰는 ItemWriter
  • transaction-manager : 스프링의 PlatformTransactionManager로 배치작업 중 트랜잭션을 시작하고 커밋하는데 사용 (default 설정은 “transactionManger”이며 생략 가능)
  • job-repository : 배치작업 중 StepExecution과 ExecutionContext을 주기적으로 저장하기 위한 저장소 (default 설정은 “jobRepository”이며 생략 가능)
  • commit-interval : 트랜잭션이 커밋되기 전 처리되어야할 item의 수

✔ ItemProcessor 속성은 옵션이며 ItemProcessor가 없는 경우 reader에서 writer로 직접 전달된다.

상속을 이용한 설정

parent

‘parent’ 속성은 Step을 상속하여 유사한 설정의 Step이 여러 개일 경우 유용하게 사용할 수 있다. Java에서 클래스 상속과 유사하게 자식 Step은 부모 Step의 속성들과 자신의 속성들을 결합한다. 또한 부모 Step의 속성을 오버라이드 하여 사용할 수도 있다. 아래 예제에서 “parentStep"을 상속받은 “concreteStep1"은 ‘itemReader’, ‘itemProcessor’, ‘itemWriter’, startLimit=5, allowStartIfComplete=true로 설정되며, commit-interval은 5로 오버라이드하여 설정된다.

<step id="parentStep">
	<tasklet allow-start-if-complete="true">
		<chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
	</tasklet>
</step>

<step id="concreteStep1" parent="parentStep">
	<tasklet start-limit="5">
		<chunk processor="itemProcessor" commit-interval="5"/>
	</tasklet>
</step>
abstract

Java의 Abstract 클래스와 동일한 개념으로 때로는 완전한 Step을 구성하지 않는 부모 Step의 정의가 필요할 때가 있다. ‘abstract’ 속성은 Step 설정이 추상레벨인지 여부를 지정한다. 아래 예제에서 “abstractParentStep"은 abstract 선언되었으며 자식 Step인 “concreteStep2"에서는 Step 설정의 필수요소인 ‘itemReader’, ‘itemWriter’, ‘commitInterval’를 정의해야 한다.

<step id="abstractParentStep" abstract="true">
	<tasklet>
		<chunk commit-interval="10"/>
	</tasklet>
</step>

<step id="concreteStep2" parent="abstractParentStep">
	<tasklet>
		<chunk reader="itemReader" writer="itemWriter"/>
	</tasklet>
</step>
merge

부모 Step을 상속받아 자식 Step에서 동일한 속성을 정의하는 경우 기본적으로 오버라이딩된다. 그러나 ‘merge’ 속성을 이용해 자녀 Step이 부모 Step에 의해 정의 된 리스너에 추가 리스너를 추가 할 수 있다.(<listeners>를 포함한 list 속성에서 사용 가능) 아래 예제에서 “concreteStep3” Step은 “listenerTwo”와 “listenerOne”를 모두 사용할 수 있다

<step id="listenersParentStep" abstract="true">
	<listeners>
		<listener ref="listenerOne"/>
	<listeners>
</step>

<step id="concreteStep3" parent="listenersParentStep">
	<tasklet>
		<chunk reader="itemReader" writer="itemWriter" commit-interval="5"/>
	</tasklet>
	<listeners merge="true">
	<listener ref="listenerTwo"/>
	<listeners>
</step>

Restart를 위한 Step 설정

start-limit

각 Step 실행 횟수를 설정한다. default는 SimpleStepFactoryBean 클래스에서 셋팅 되며 Integer.MAX_VALUE로 설정 되있다.

allow-start-if-complete

Job의 Restart 시, “COMPLETED”로 완료한 Step의 실행 여부를 설정한다. true로 설정 시, “COMPLETED”로 완료한 Step도 다시 실행되며 이전 시도의 결과를 오버라이드 한다. (false 설정 시, “COMPLETED”로 완료한 Step은 skip)

아래 예제에서 “step1” Step은 10번만 실행 가능하며 Job을 Restart 했을 시, 이전 시도와 관계없이 재실행 된다.있다

<step id="step1">
	<tasklet allow-start-if-complete="true" start-limit="10">
		<chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
	</tasklet>
</step>

Skip/Retry/Repeat 설정

Skip

Skip 설정 자세히 보기

Retry

Retry 설정 자세히 보기

Step 흐름제어

Step 흐름제어(Flow Control) 자세히 보기

Step 흐름제어

변수 선언 후 Listeners를 통해서 모든 Setp에서 사용자 정의 변수를 사용할 수 있도록 EgovStepVariableListener를 통해서 지원한다.

Step Variable 설정 자세히 보기

참고자료

7.7 - Step Variable

EgovStepVariableListener를 통해 변수를 선언하고 여러 Step에서 사용자 정의 변수를 공유하여 사용할 수 있는 기능을 지원한다. 이를 통해 배치 실행 중 여러 Step에서 변수를 공유하며 사용할 수 있다.

Step Variable

개요

변수 선언 후 Listeners를 통해서 모든 Setp에서 사용자 정의 변수를 사용할 수 있도록 EgovStepVariableListener를 통해서 지원한다. 사용자가 변수를 정의하여 여러 step에서 해당 변수를 공유하여 사용 가능한 기능으로 이루어져있다.

step-variable-architecture6

설명

Step Variable 설정

배치실행환경에서 제공하는 EgovJobVariableListener 사용하여 사용자 정의 변수를 설정한다.

<bean id="egovStepVariableListener" class="egovframework.rte.bat.support.EgovStepVariableListener">
<property name="pros">
<props>
	<prop key="StepVariableKey1">StepVariableValue1</prop>
	<prop key="StepVariableKey2">StepVariableValue2</prop>
	<prop key="StepVariableKey3">StepVariableValue3</prop>
</props>
</property>
</bean>

job, step 설정

step 설정시 listener를 사용하여 공유변수 서비스를 설정한다.

<job id="delimitedToDelimitedJob-StepVariable" parent="eGovBaseJob" xmlns="http://www.springframework.org/schema/batch">
	<step id="step1">
		<tasklet ref="taskletStep" />
		<listeners>
			<listener ref="egovStepVariableListener" />
		</listeners>
	</step>
</job>

step에서 tasklet 선언시 Step Variable 사용

setter 방식으로 공유변수 사용시 아래와 같이 응용하여 설정한다.

<bean id="taskletStep" class="egovframework.example.bat.step.TaskletStep" scope="step">
	<property name="stepVariable" value="#{stepExecutionContext[StepVariableKey1]}" />
</bean>

Setp 처리시 Step Variable 사용

public class TaskletStep implements Tasklet, InitializingBean {
 
	private String stepVariable;
 
	@Value("#{stepExecutionContext[StepVariableKey2]}")		
	private String vStepVariable;
 
	public String getStepVariable() {
		return stepVariable;
	}
 
	public void setStepVariable(String stepVariable) {
		this.stepVariable = stepVariable;
	}
 
	@Override
	public RepeatStatus execute(StepContribution contribution,
	               ChunkContext chunkContext) throws Exception {
 
		//Tasklelt 선언시 setter의해 선언된  Step Variable : stepVariable
 
		//annotation @Value 통해 선언된  Step Variable : vStepVariable
 
		//direct 접근을 통한 Step Variable 사용 : chunkContext.getStepContext().getStepExecutionContext().get("StepVariableKey3")
 
		return RepeatStatus.FINISHED;

7.8 - ItemReader

ItemReader는 다양한 데이터 타입(플랫 파일, XML, 데이터베이스)을 처리하며, 데이터를 한 항목씩 읽고 모두 소진되면 Null을 반환하는 인터페이스이다. 플랫 파일, XML, 데이터베이스 각각에 맞는 방식으로 데이터를 읽고 처리할 수 있도록 지원한다.

ItemReader

개요

ItemReader는 읽기 대상의 타입에 관계없이 한번에 한 항목을 읽으며 읽을 항목이 모두 소진되면 Null을 반환하는 인터페이스이다.

설명

ItemReader는 여러 종류의 데이터 타입을 입력 받을 수 있다. 가장 일반적인 데이터 타입으로 플랫 파일, XML, 데이터베이스가 있다.

  • 플랫 파일 : 플랫 파일 ItemReader는 일반적으로 고정 위치로 정의된 데이터 필드나 특수 문자에 의해 구별된 데이터의 행을 읽는다.
  • XML : XML ItemReader는 파싱, 매핑, 유효성 검증을 XML에서 독립적으로 작업할 수 있도록 처리해준다. 입력 데이터는 XSD 스키마에 대해 XML 파일의 유효성 검증이 가능하다.
  • 데이터베이스 : 데이터베이스 ItemReader는 데이터베이스 리소스에 객체로 맵핑 될 수 있는 resultset으로 반환하여 접근한다. 기본 SQL ItemReaders는 객체를 반환하는 RowMapper를 호출한다.

기본적인 ItemReader 인터페이스는 아래와 같다.

public interface ItemReader<T> {
 
   T read() throws Exception, UnexpectedInputException, ParseException;
 
}

read() 메소드는 ItemReader의 필수적인 메소드이며 결과값으로 하나의 item을 반환하고 더이상 반환할 item이 없을 경우 null을 반환한다. item은 플랫 파일에서의 한 라인, 데이터베이스에서의 한 행, XML 파일에서의 엘리먼트를 나타낸다.

FlatFile ItemReader

플랫파일은 2차원 데이터를 포함하는 유형의 파일이다. 스프링 배치 프레임워크에서는 플랫파일을 읽고 파싱하는 기본적인 기능을 제공하는 FlatFileItemReader 클래스를 통해 플랫파일에 대한 읽기 처리를 한다.

FlatFileItemReader

FlatFileItemReader는 Resource, LineMapper, FieldSetMapper, LineTokenizer에 기본적으로 의존성을 갖으며, LineTokenizer에 따라 구분자(Delimited)와 고정길이(Fixed Length) 방식으로 FlatFileItemReader를 사용할 수 있다.

image

구분데이터 형태설명
LineMapper플랫파일 1 라인(String) → Object플랫파일 데이터에서 읽은 1 라인(String)을 Object로 변환하는 총 과정(LineTokenizer, FieldSetMapper 과정을 포함한다.)
LineTokenizerString → Tokens → FieldSet플랫파일에서 읽은 1 라인(String)을 구분자 방식 또는 고정길이 방식으로 토크나이징 한 후 FieldSet 형태로 변환하는 과정
DelimitedLineTokenizer (구분자) : 1 라인의 String을 구분자 기준으로 나누어 토큰화 하는 방식
EgovEscapableDelimitedLineTokenizer : Escape 문자를 사용하여 Delimiter(구분자) 문자를 문자열에 추가할 수 있는 방식 (예: 1”,000원 → 1,000원 으로 인식)
FixedLengthTokenizer (고정길이) : 1 라인의 String을 사용자가 설정한 고정길이 기준으로 나누어 토큰화 하는 방식
FieldSetMapperFieldSet → ObjectFieldSet 형태의 데이터를 원하는 Object로 변환하는 과정

아래 Delimited(구분자), Fixed Length(고정길이) 방식으로 설정한 FlatFileItemReader의 예시를 통해 FlatFileItemReader, LineMapper, LineTokenizer, FieldSetMapper의 의존 관계를 볼 수 있다.

Tokenizing 방식설정
Delimited (구분자)
<bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step">
	<property name="resource" value="#{jobParameters[inputFile]}" />
	<property name="lineMapper">
		<bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
			<property name="lineTokenizer">
				<bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
					<property name="delimiter" value=","/>
					<property name="names" value="name,credit" />
				</bean>
			</property>
			<property name="fieldSetMapper">
				<bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
					<property name="targetType" value="org.springframework.batch.CustomerCredit" />	
				</bean>
			</property>
		</bean>
	</property>
</bean>
Tokenizing 방식설정
EscapableDelimited
<<bean id="delimitedToDelimitedJob-EscapeCharacter-delimitedItemReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step">		
	<property name="resource" value="#{jobParameters[inputFile]}" />
	<property name="lineMapper">
		<bean class="egovframework.rte.bat.core.item.file.mapping.EgovDefaultLineMapper">
			<property name="lineTokenizer">
			<bean class="egovframework.rte.bat.core.item.file.transform.EgovEscapableDelimitedLineTokenizer">
				<property name="delimiter" value="," />
				<property name="escape" value="true" />
				<property name="quoteCharacter" value="&quot;" />						
			</bean>
			</property>
			<property name="objectMapper">
				<bean class="egovframework.rte.bat.core.item.file.mapping.EgovObjectMapper">
					<property name="type" value="egovframework.example.bat.domain.trade.CustomerCreditMore" />
					<property name="names" value="id,name,credit,serial,tax,amount,createDate,changeDate" />
				</bean>
			</property>
		</bean>
	</property>
</bean>
Fixed Length (고정길이)
<bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step">
	<property name="resource" value="#{jobParameters[inputFile]}" />
	<property name="lineMapper">
		<bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
			<property name="lineTokenizer">
				<bean class="org.springframework.batch.item.file.transform.FixedLengthTokenizer">
					<property name="columns" value="1-9,10-11" />
					<property name="names" value="name,credit" />
				</bean>
			</property>
			<property name="fieldSetMapper">
				<bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
					<property name="targetType" value="org.springframework.batch.CustomerCredit" />	
				</bean>
			</property>
		</bean>
	</property>
</bean>

LineTokenizer와 FieldSetMapper에 아래와 같은 항목을 설정해야한다.

설정항목내용
targetTypeVO 클래스를 나타낸다.
namesVO 클래스의 필드를 나타낸다.

✔ 사용하는 LineTokenizer에 따라 설정항목이 다르므로 주의하여 설정해야 한다.

LineTokenizer설정항목설명설정 예
DelimitedLineTokenizerdelimiter토크나이징 할 때 기준이 되는 구분자 설정,
EgovEscapableDelimitedLineTokenizerdelimiter토크나이징 할 때 기준이 되는 구분자 설정,
escapeEscape 문자 사용 설정true 또는 false
quoteCharacter사용 할 Escape 문자 설정쌍따움표(”, ")
XML escape문자로 인하여 쌍따움표는 " 표시
FixedLengthTokenizercolumns토크나이징 할 때 기준이 되는 고정길이 설정1-9,10-11

XML ItemReader

스프링 배치는 XML 레코드를 읽고 자바 객체로 매핑하는 작업에 대해 트랜잭션 인프라스트럭쳐를 제공한다. 스프링 배치에서 XML 입력과 출력이 어떻게 작동되는지 더 살펴보면 첫째로 파일 읽기 및 쓰기에 따라 차이가 있지만 스프링 배치 XML 처리 과정은 공통화 돼있다. XML 처리 과정에서 토크나이징이 필요한 레코드(FieldSets) 라인 대신 개별 레코드와 대응되는 ‘fragments’의 콜렉션으로 가정하고 있다.

image

위의 시나리오에서 ’trade’ 태그는 ‘루트 엘리먼트’로 정의 되었다. ‘<trade>‘와 ‘</trade>’ 사이의 모든 내용은 하나의 fragment로 여겨진다. 스프링 배치는 fragment를 객체로 바인드 하는데 Object/XML Mapping (OXM)을 사용한다. 하지만 스프링 배치는 특정 XML 바인딩 기술에 묶여있지 않다. 대표적인 사용방법은 가장 대중적인 OXM 기술에 대한 일관된 추상화를 제공하는 스프링 OXM에 위임하는 방법이다. 스프링 OXM에 대한 의존성은 선택적이며 만일 필요하다면 스프링 배치에서 특정 인터페이스를 구현하도록 선택할 수 있다. XML 지원 관련 기술 관계는 아래 그림과 같다.

image

StaxEventItemReader

StaxEventItemReader 설정은 XML 입력 스트림에서 레코드의 처리를 위한 전형적인 설정을 제공한다. 먼저, StaxEventItemReader가 처리할 수 있는 XML 레코드 집합을 검토해보자.

<?xml version="1.0" encoding="UTF-8"?>
<records>
   <trade xmlns="http://springframework.org/batch/sample/io/oxm/domain">
      <isin>XYZ0001</isin>
      <quantity>5</quantity>
      <price>11.39</price>
      <customer>Customer1</customer>
   </trade>
   <trade xmlns="http://springframework.org/batch/sample/io/oxm/domain">
      <isin>XYZ0002</isin>
      <quantity>2</quantity>
      <price>72.99</price>
      <customer>Customer2c</customer>
   </trade>
   <trade xmlns="http://springframework.org/batch/sample/io/oxm/domain">
      <isin>XYZ0003</isin>
      <quantity>9</quantity>
      <price>99.99</price>
      <customer>Customer3</customer>
   </trade>
</records>

XML 레코드를 처리하기 위해서는 다음 사항이 필요하다.

  • 루트 엘리먼트 명 : 매핑되는 객체를 구성하는 fragment의 루트 엘리먼트 명. 위의 예에서는 trade가 루트 엘리먼트 명이다.
  • Resource : 읽어들일 데이터의 위치를 지정(파일이나 URL 등)
  • FragmentDeserializer : XML fragment를 객체로 매핑하는 스프링 OXM에 의해 제공된 언마샬링 기능
<bean id="itemReader" class="org.springframework.batch.item.xml.StaxEventItemReader">
   <property name="fragmentRootElementName" value="trade" />
   <property name="resource" value="data/iosample/input/input.xml" />
   <property name="unmarshaller" ref="tradeMarshaller" />
</bean>

아래 예제에서는 XStreamMarshaller를 사용한다. XStreamMarshaller는 fragment 명과 객체 타입을 바인드 해주기 위해 사용하는 별칭을 키와 값을 포함하는 맵으로 건내주도록 했다. 그 다음 FieldSet과 비슷하게 맵에 엘리먼트 이름과 타입이 키/값 쌍으로 들어가게 된다. 다음처럼 설정 파일에서 필요한 별칭을 기술하는데 스프링 설정 유틸리티를 사용할 수 있다.

<bean id="tradeMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
   <property name="aliases">
      <util:map id="aliases">
         <entry key="trade" value="org.springframework.batch.sample.domain.Trade" />
         <entry key="price" value="java.math.BigDecimal" />
         <entry key="name" value="java.lang.String" />
      </util:map>
   </property>
</bean>

입력 리더는 (기본적으로 태그 이름의 일치에 의해서) 새로운 프레그먼트가 시작하는 것을 인식할 때까지 XML 자원을 읽어 들인다. 리더는 프레그먼트에서 독립적으로 작동하는 XML 문서를 생성하고, XML을 자바 객체로 매핑하기 위해 deserializer에게 이 문서를 전달한다. 정리해보면,

StaxEventItemReader xmlStaxEventItemReader = new StaxEventItemReader()
Resource resource = new ByteArrayResource(xmlResource.getBytes())
 
Map aliases = new HashMap();
aliases.put("trade","org.springframework.batch.sample.domain.Trade");
aliases.put("price","java.math.BigDecimal");
aliases.put("customer","java.lang.String");
Marshaller marshaller = new XStreamMarshaller();
marshaller.setAliases(aliases);
xmlStaxEventItemReader.setUnmarshaller(marshaller);
xmlStaxEventItemReader.setResource(resource);
xmlStaxEventItemReader.setFragmentRootElementName("trade");
xmlStaxEventItemReader.open(new ExecutionContext());
 
boolean hasNext = true
 
CustomerCredit credit = null;
 
while (hasNext) {
   credit = xmlStaxEventItemReader.read();
   if (credit == null) {
      hasNext = false;
   }
   else {
      System.out.println(credit);
   }
}

Database ItemReader

대부분의 엔터프라이즈 애플리케이션처럼 데이터베이스는 배치 저장 메카니즘의 중심이 된다. 그러나 배치는 다른 애플리케이션 스타일과는 다르다. 만일 SQL 문이 백만 행을 반환하는 경우에 결과 집합은 모든 행을 읽을 때까지 메모리에 모든 결과를 보유한다. 스프링 배치는 이 문제를 해결하기 위해 Cursor와 Paging 데이터베이스 ItemReader를 제공한다.

커서(Cursor) 기반 ItemReader

커서 방식을 이용한 데이터베이스 접근 방식은 가장 기본적인 방식이다. 왜냐하면 ‘streaming’ 관계형 데이터의 문제에 대한 해결책이기 때문이다. Java의 ResultSet 클래스는 커서를 다루는 객체 지향 메커니즘의 필수적인 클래스이다. ResultSet은 데이터의 현재 행에 커서를 유지하며 다음 데이터를 호출하면 다음 행으로 커서를 이동한다. 스프링 배치에서 커서는 커서를 초기화해서 열어주는 ItemReader에 기반하며, read가 호출될 떄마다 커서를 다음 행으로 이동시키며 처리 과정 중에 사용되는 맵핑된 객체를 반환한다.

아래 그림의 예제는 커서 기반의 ItemReader의 작동을 보여준다. ‘FOO’ 테이블은 ID, NAME, BAR 세 개의 컬럼을 갖는다. SQL문을 통해 ID가 1보다 크고 7보다 작은 행의 결과를 조회한다. 커서는 ID 2에서 시작하며 read()가 호출될 떄마다 FOO 객체로 맵핑되고 커서는 다음 행으로 이동한다.

image

JdbcCursorItemReader

JdbcCursorItemReader는 커서 기반 기술의 JDBC를 구현한 ItemReader이다. 아래의 JdbcCursorItemReader의 설정 예시를 보면 굉장히 쉽게할 수 있음을 알 수 있다. dataSouce 속성으로 DB connection을 넣어올 수 있는 datasource를 지정하고, sql 속성에 실행할 쿼리, rowMapper 속성에 ResultSet에서 객체를 매핑하는 클래스로 RowMapper 인터페이스를 구현한 클래스가 필요하다.

<bean id="itemReader" class="org.springframework.batch.item.database.JdbcCursorItemReader">
   <property name="dataSource" ref="dataSource"/>
   <property name="sql" value="select ID, NAME, CREDIT from CUSTOMER"/>
   <property name="rowMapper">
      <bean class="egovframework.brte.sample.domain.trade.CustomerCreditRowMapper"/>
   </property>
</bean>

페이징(Paging) 기반 ItemReader

데이터베이스 커서를 사용하는 대신 여러번 쿼리를 실행할 수 있는데 실행되는 각 쿼리는 정해진 크기인 페이지만큼의 결과를 가져오게 된다. 실행되는 각 쿼리는 시작 행 번호를 지정하고 페이지에 반환시키고자 하는 행의 수를 지정한 후 사용한다.

JdbcPagingItemReader

페이징 ItemReader의 구현체 중 하나인 JdbcPagingItemReader는 페이지를 형성하는 행을 반환하는데 사용하는 SQL 쿼리를 제공할 책임을 지고 있는 PagingQueryProvider 인터페이스가 필요하다. 데이터베이스 유형 별로 지원하는 OraclePagingQueryProvider, HsqlPagingQueryProvider, MySqlPagingQueryProvider ,SqlServerPagingQueryProvider,SybasePagingQueryProvider 등의 구현를 사용하지만 데이터베이스를 자동으로 식별해주고 적절한 PagingQueryProvider 구현체를 적용해주는데 사용하는 SqlPagingQueryProviderFactoryBean이 있다. SqlPagingQueryProviderFactoryBean는 환경 설정을 간단히 해주며 추천하는 구현체이다.

아래 예제는 위의 JdbcCursorItemReader 설정과 동일한 설정이다.

<bean id="itemReader" class="org.springframework.batch.item.database.JdbcPagingItemReader">
   <property name="dataSource" ref="dataSource"/>
   <property name="queryProvider">
      <bean class="org.springframework.batch.item.database.support.SqlPagingQueryProviderFactoryBean">
         <property name="dataSource" ref="dataSource" />
         <property name="selectClause" value="select id, name, credit"/>
         <property name="fromClause" value="from customer"/>
         <property name="whereClause" value="where status=:status"/>
         <property name="sortKey" value="id"/>
      </bean>
   </property>
   <property name="parameterValues">
      <map>
         <entry key="status" value="NEW"/>
      </map>
   </property>
   <property name="pageSize" value="1000"/>
   <property name="rowMapper" ref="customerMapper"/>
</bean>
IbatisPagingItemReader

iBatis를 사용해 데이터에 접근하는 경우 페이징 ItemReader를 구현한 IbatisPagingItemReader를 사용할 수 있다. iBatis는 페이지의 행을 읽을 수 있는 직접적인 지원은 하지 않지만 여러 표준화된 변수를 이용해여 쿼리를 추가할 수 있다.

아래 설정은 위에서 설명한 JdbcCursorItemReader, JdbcCursorItemReader 설정과 같은 설정을 보여준다.

<bean id="itemReader" class="org.springframework.batch.item.database.IbatisPagingItemReader">
   <property name="sqlMapClient" ref="sqlMapClient"/>
   <property name="queryId" value="getPagedCustomerCredits"/>
   <property name="pageSize" value="1000"/>
</bean>

위의 IbatisPagingItemReader 설정에서 사용한 queryId 속성의 “getPagedCustomerCredits”의 구성은 아래와 같다. ex) MySQL

<select id="getPagedCustomerCredits" resultMap="customerCreditResult">
   select id, name, credit from customer order by id asc LIMIT #_skiprows#, #_pagesize#
</select>

전자정부에서 제공하는 eGovFlatFileItemReader

스프링 배치에서 제공하는 파일 기반 관련설정을 사용할 경우, 대용량 데이터 처리 시간이 상용 배치프레임워크 대비 성능이 떨어졌다. 이 문제를 해결하기 위해서 전자정부에서는 파일 ItemReader의 요소 중 성능저하 요인인 LineMapper 부분을 개선하여 제공한다.

아래 그림을 보면 전자정부에서는 FieldSet을 사용하지 않는다. 따라서 Tokens → FieldSet으로 변환하는 과정이 없다. 전자정부에서 제공하는 EgovDefaultLineMapper, EgovLineTokenizer, EgovObjectMapper를 사용하는 경우 Tokens 상태에서 Object로 직접 맵핑된다.

스프링 FlatFileItemReader 구조전자정부 eGovFlatFileItemReader 구조
imageimage
개선사항설명
EgovDefaultLineMapperEgovLineTokenizer와 EgovObjectMapper가 변경됨에 따라 LineMapper 총 과정을 제어하는 DefaultLineMapper를 변경하여 EgovDefaultLineMapper 제공
EgovLineTokenizer전자정부에서는 FieldSet을 사용하지 않기때문에 FieldSet을 반환하는 LineTokenizer 인터페이스를 변경하여 EgovLineTokenizer 제공
EgovAbstractLineTokenizerLineTokenizer 인터페이스가 EgovLineTokenizer로 변경됨에 따라 토크나이징만 관여하는 추상 클래스 EgovAbstractLineTokenizer 제공
EgovDelimitedLineTokenizer스프링에서 제공하는 DelimitedLineTokenizer의 성능을 개선한 EgovDelimitedLineTokenizer 제공
EgovObjectMapper전자정부에서는 FieldSet을 사용하지 않고 토크나이징 된 값들을 직접 Object에 맵핑하는 EgovObjectMapper를 제공

아래의 XML 설정은 스프링에서 제공하는 DefaultLineMapper를 적용한 FlatFileItemReader와 전자정부에서 제공하는 EgovDefaultLineMapper를 적용한 FlatFileItemReader 설정 비교이다.

✔ 주의! EgovDefaultLineMapper 사용 시, 반드시 EgovTokenizer(EgovFixedLengthTokenizer, EgovByteLengthTokenizer, EgovDelimitedTokenizer)와 EgovObjectMapper를 사용해야 한다.

✔ 주의! EgovObjectMapper 사용 시, VO 필드 타입은 String, int, double, float, long, char, boolean, short, BigDecimal로 제한된다.

✔ 주의! 스프링의 DefaultLineMapper 사용 시, Tokenizer에서 ’names’ 속성을 설정하지만 전자정부의 EgovDefaultLineMapper 사용 시, EgovObjectMapper에서 ’names’ 속성을 설정한다.

EgovObjectMapper 설정항목

Delimited(구분자) 방식 설정

읽어들인 문자열에서 구분자를 경계값으로 사용하여 필드를 분리한다.

구분설정
스프링 FlatFileItemReader
<bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step">
    <property name="resource" value="#{jobParameters[inputFile]}" />
    <property name="lineMapper">
        <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
	     <property name="lineTokenizer">
		  <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
		       <property name="delimiter" value=","/>
		       <property name="names" value="name,credit" />
		  </bean>
	     </property>
	     <property name="fieldSetMapper">
		  <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
		       <property name="targetType" value="egovframework.brte.sample.domain.trade.CustomerCredit" />	
		  </bean>
	     </property>
	</bean>
    </property>
</bean>
전자정부 EgovFlatFileItemReader
<bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step">
     <property name="resource" value="#{jobParameters[inputFile]}" />
     <property name="lineMapper">
          <bean class="egovframework.brte.core.item.file.mapping.EgovDefaultLineMapper">
              <property name="lineTokenizer">
                  <bean class="egovframework.brte.core.item.file.transform.EgovDelimitedLineTokenizer">
                      <property name="delimiter" value=","/>
                  </bean>
              </property>
              <property name="objectMapper">
                  <bean class="egovframework.brte.core.item.file.mapping.EgovObjectMapper">
                      <property name="type" value="egovframework.brte.sample.domain.trade.CustomerCredit" />
                      <property name="names" value="name,credit" />
                  </bean>
              </property>        
          </bean>
     </property> 
</bean>
EgovFlatFileItemReader 설정항목내용예시
delimiter필드의 경계를 구별해주는 문자를 나타낸다., (콤마)
typeVO 클래스를 나타낸다.org.springframework.batch.CustomerCredit
namesVO 클래스의 필드를 나타낸다.name,credit
Fixed Length(고정길이) 방식 설정

읽어들인 문자열에서 필드의 경계를 파일 내의 문자열 길이로 판단하여, 필드를 분리한다.

구분설정
스프링 FlatFileItemReader
<bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step">
    <property name="resource" value="#{jobParameters[inputFile]}" />
    <property name="lineMapper">
        <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
	    <property name="lineTokenizer">
		 <bean class="org.springframework.batch.item.file.transform.FixedLengthTokenizer">
		     <property name="columns" value="1-9,10-11" />
		     <property name="names" value="name,credit" />
		 </bean>
	    </property>
	    <property name="fieldSetMapper">
		 <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
		     <property name="targetType" value="egovframework.brte.sample.domain.trade.CustomerCredit" />	
		 </bean>
	    </property>
	</bean>
    </property>
</bean>
전자정부 EgovFlatFileItemReader
<bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step">
    <property name="resource" value="#{jobParameters[inputFile]}" />
    <property name="lineMapper">
        <bean class="egovframework.brte.core.item.file.mapping.EgovDefaultLineMapper">
            <property name="lineTokenizer">
                <bean class="egovframework.brte.core.item.file.transform.EgovFixedLengthTokenizer">
                    <property name="columns" value="1-9,10-11" />
                </bean>
            </property>
            <property name="objectMapper">
                <bean class="egovframework.brte.core.item.file.mapping.EgovObjectMapper">
                    <property name="type" value="egovframework.brte.sample.domain.trade.CustomerCredit" />
                    <property name="names" value="name,credit" />
                </bean>
            </property>        
        </bean>
    </property>      
</bean>
EgovFlatFileItemReader 설정항목내용예시
column필드 경계의 범위를 나타낸다.1-9,10-11
typeVO 클래스를 나타낸다.org.springframework.batch.CustomerCredit
namesVO 클래스의 필드를 나타낸다.name,credit
ByteLength 방식 설정

전자정부에서는 EgovFixedByteLengthTokenizer를 추가적으로 제공한다. EgovFixedByteLengthTokenizer는 기본적으로 FixedLengthTokenizer와 유사하나, byte 문자열을 기준으로 필드의 경계값을 구해 필드를 분리한다.

구분설정
전자정부 EgovFlatFileItemReader
<bean id="itemReader" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step">
    <property name="resource" value="#{jobParameters[inputFile]}" />
    <property name="lineMapper">
        <bean class="egovframework.brte.core.item.file.mapping.EgovDefaultLineMapper">
            <property name="lineTokenizer">
                <bean class="egovframework.brte.core.item.file.transform.EgovFixedByteLengthTokenizer">
                    <property name="byteEncoding" value="utf-8"/>
                    <property name="columns" value="1-9,10-11" />
                </bean>
            </property>
            <property name="objectMapper">
                <bean class="egovframework.brte.core.item.file.file.mapping.EgovObjectMapper">
                    <property name="type" value="egovframework.brte.sample.domain.trade.CustomerCredit" />
                    <property name="names" value="name,credit" />
                </bean>
            </property>        
        </bean>
    </property>      
</bean>
EgovFlatFileItemReader 설정항목내용예시
column필드 경계의 길이를 나타낸다.1-9,10-11
byteEncodingbyte 문자열의 인코딩 타입을 나타낸다.utf-8
typeVO 클래스를 나타낸다.org.springframework.batch.CustomerCredit
namesVO 클래스의 필드를 나타낸다.name,credit

전자정부에서 제공하는 eGovIndexFileReader

배치 Job 정의 시 Resource 엘리먼트의 shell step에 shell script에 포함된 파일명에서 일련번호(index)를 사용할 수 있는 Reader를 제공한다.

Index 파일명을 사용하면 파일의 일련번호를 기준으로 동적인 파일명 생성이 가능하다.

Index(NDX) 파일명 치환 로직

NDX File : 파일 이름이 “[이름]_NDX_[YYYYMMDDhhmmss]” 형식으로 이루어진 파일
           ex) Sample_NDX_20121126151237

NDX 일련번호 : 파일명 끝에 14자리 수의 생성시간(년월일시분초)

NDX 파일명 치환 : “[이름]_NDX(Index)” 형식의 파일명은 해당 디렉터리의 NDX 파일에 대해 Index에 해당하는 실제 파일명으로 치환됨

(-2 ) : 일련번호 기준 마지막 파일에서 두 번째 이전 파일명으로 치환됨
(-1 ) : 일련번호 기준 마지막 파일에서 첫 번째 이전 파일명으로 치환됨
( 0 ) : 일련번호 기준 마지막 파일명으로 치환됨
(+1 ) : 일련번호 기준 마지막 파일에서 Index를 1 증가시켜 새로운 파일 생성

NDX 파일목록 중 잘못된 파일명이 존재할 경우 에러를 발생한다.

구분예시비고
에러Sample_NDX_20180104index 자릿수가 10자리
에러Sample_NDX_000Aindex는 숫자만 허용함
무시Sample_20180104123456NDX 파일이 아닌 일반 파일로 인식

Index(NDX) Reader 방식 설정

Property(indexResource)의 파일을 NDX파일 설정에 따라 읽어드린다.

<bean id="fileIndex-delimitedItemReader" class="egovframework.rte.bat.core.item.file.EgovIndexFileReader">
	<property name="indexResource" value="file:./src/main/resources/egovframework/batch/data/inputs/csvData_NDX(0)" />
	   <property name="lineMapper">
		<bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
			<property name="lineTokenizer">
				<bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
					<property name="delimiter" value="," />
					<property name="names" value="name,credit" />
				</bean>
				</property>
				<property name="fieldSetMapper">
				<bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
					<property name="targetType" value="egovframework.example.bat.domain.trade.CustomerCredit" />
				</bean>
			</property>
		</bean>
	</property>
</bean>
EgovIndexFileReader 설정항목내용예시
indexResource읽어올 index(NDX)파일을 설정한다.Index(NDX) 파일명 치환 로직 참조

전자정부에서 제공하는 EgovMyBatisPagingItemReader

배치 처리시 Paging방식으로 mybatis에서 데이터를 읽기 위해 EgovMyBatisPagingItemReader 서비스를 제공합니다.(mybatis MyBatisPagingItemReader 클래스를 확장한 서비스) 실행환경 제공 Resource Variable, Step Variable, Job Variable 서비스와 함께 사용 가능하지만 parameterValues 서비스와 함께 사용은 불가능하다.

image

EgovMyBatisPagingItemReader 설정항목

설정항목내용예시
sqlSessionFactoryreader에 별도로 구현한 sessionFactorysqlSession
parameterValues파라미터 전달을 위한 설정parameterValues
queryId네임스페이스를 가진 매퍼 파일을 Query Id.EmpMapper.selectEmpList
scope해당 Reader가 적용될 Bean Scopestep, job
pageSize배치가 처리할 페이지 사이즈 크기#{100}
resourceVariable표준프레임워크 실행환경 Resource Variable 서비스를 사용하기 위한 설정resourceVariable
jobVariable표준프레임워크 실행환경 Step Variable 서비스를 사용하기 위한 설정jobVariable
stepVariable표준프레임워크 실행환경 Job Variable 서비스를 사용하기 위한 설정stepVariable

EgovMyBatisPagingItemReader 설정항목 설정

<bean id="mybatisJobStep.mybatisItemReader" class="egovframework.rte.bat.item.database.EgovMyBatisPagingItemReader" scope="step">
	<property name="sqlSessionFactory" ref="sqlSession" />
	<property name="resourceVariable" ref="resourceVariable" />
	<property name="jobVariable" ref="jobVariable" />
	<property name="stepVariable" ref="stepVariable" />
	<property name="queryId" value="EmpMapper.selectEmpList" />
	<property name="pageSize" value="#{100}" />
</bean>

참고자료

7.9 - ItemWriter

ItemWriter는 다양한 데이터 타입에 상관없이 한 번에 여러 항목(Chunk)을 쓰는 역할을 하며, ItemReader와 유사하지만 데이터를 쓰는 반대의 동작을 수행한다. 이를 통해 데이터를 효과적으로 처리하고 저장할 수 있다.

ItemWriter

개요

ItemWriter는 대상 타입에 관계없이 한번에 항목의 묶음(Chunk)을 쓰는 동작의 인터페이스이다.

설명

ItemWriter의 기능은 ItemReader와 유사하지만 정반대의 동작을 한다. 기본적인 ItemWriter 인터페이스는 아래와 같다.

public interface ItemWriter<T> {
 
   void write(List<? extends T> items) throws Exception;
 
}

write() 메소드는 ItemWriter의 필수적인 메소드이며 인자로 건넨 객체가 열려 있는 동안 쓰기 작업을 시도한다.

FlatFile ItemWriter

FlatFileItemWriter는 Resource, LineAggregator에 기본적으로 의존성을 갖으며, LineAggregator에 따라 구분자(Delimited)와 고정길이(Fixed Length) 방식으로 쓸 수 있다.

image

구분데이터 형태설명
LineAggregatorItem → StringItemReader, ItemProcessor 과정을 거친 Item을 1 라인의 String으로 변환하는 총 과정(FieldExtractor 과정을 포함한다.)
DelimitedLineAggregator (구분자) : Item의 Field와 Field 사이에 구분자를 삽입하여 1라인의 String으로 변환하는 과정
FormatterLineAggregator(고정길이 포맷) : Item의 Field를 사용자가 설정한 포맷 기준으로 1라인의 String으로 변환하는 과정
FieldExtractorItem → FieldsItem에서 Field 값들을 꺼내어 Object[]로 변환하는 과정

아래 Delimited(구분자), Fixed Length(고정길이) 방식으로 설정한 FlatFileItemWriter의 예시를 통해 FlatFileItemWriter, LineAggregator, FieldExtractor의 의존 관계를 볼 수 있다.

Aggregate 방식설정
Delimited (구분자)
<bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step">
    <property name="resource" value="#{jobParameters[outputFile]}" />
    <property name="lineAggregator">
	<bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
	    <property name="delimiter" value=","/>
	    <property name="fieldExtractor">
		<bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
		    <property name="names" value="name,credit"/>					
		</bean>
	    </property>
	</bean>
    </property>
</bean>
Fixed Length (고정길이)
<bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step">
    <property name="resource" value="#{jobParameters[outputFile]}" />
    <property name="lineAggregator">
	<bean class="org.springframework.batch.item.file.transform.FormatterLineAggregator">
	    <property name="format" value="%-9s%-2s" />
	    <property name="fieldExtractor">
		<bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
		    <property name="names" value="name,credit"/>					
		</bean>
	    </property>
	</bean>
    </property>
</bean>

BeanWrapperFieldExtractor에 아래와 같은 항목을 설정해야한다.

설정항목내용
namesVO 클래스의 필드를 나타낸다.

✔ 사용하는 LineAggregator에 따라 설정항목이 다르므로 주의하여 설정해야 한다.

LineAggregator설정항목설명설정 예
DelimitedLineAggregatordelimiterItem의 필드 값들을 1 Line의 String으로 만들 때 경계가 되는 구분자 지정,(콤마)
FormatterLineAggregatorformatItem의 필드 값들을 1 Line의 String으로 만들 때 필드값의 형식과 고정길이 지정%-9s%-2s

XML ItemWriter

StaxEventItemWriter

XML 쓰는 과정은 읽기 과정에 대칭적이다. StaxEventItemWriter는 Resource, marshaller, rootTagName가 필요하다. Java 객체는 marshaller에 전달되서 OXM 도구에 의해 각 fragment마다 StartDocument와 EndDocument 이벤트를 필터링하고 커스텀 이벤트 writer를 사용해 Resource를 쓰게 된다.

아래 XStreamMarshaller를 사용한 StaxEventItemWriter 설정 예가 있다.

<bean id="itemWriter" class="org.springframework.batch.item.xml.StaxEventItemWriter">
   <property name="resource" ref="outputResource" />
   <property name="marshaller" ref="customerCreditMarshaller" />
   <property name="rootTagName" value="customers" />
   <property name="overwriteOutput" value="true" />
</bean>

marshaller가 의존성 참조를 하고 있는 customerCreditMarshaller는 다음처럼 설정하면 된다.

<bean id="customerCreditMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">
   <property name="aliases">
      <util:map id="aliases">
         <entry key="customer" value="egovframework.brte.sample.domain.trade.CustomerCredit" />
         <entry key="credit" value="java.math.BigDecimal" />
         <entry key="name" value="java.lang.String" />
      </util:map>
   </property>
</bean>

Database ItemWriter

플랫파일 및 XML 모두 특정 ItemWriter가 있지만 데이터베이스는 다르다. 왜냐하면 트랜잭션이 필요한 모든 기능을 제공하기 때문이다. 파일은 트랜잭션이 있는 것처럼 적절한 시점에 작성된 item을 추적하고 삭제하는 작업해야 하기 때문에 ItemWriter가 필요하다. 하지만 데이터베이스는 쓰기가 이미 트랜잭션에 포함되어 있기때문에 이 기능을 필요로 하지 않는다. 사용자는 ItemWriter 인터페이스를 구현해서 DAO를 만들거나 일반적인 처리 과정 관점에서 작성된 커스텀 ItemWriter 중 하나를 사용하면 된다.

JdbcBatchItemWriter

아래 XML 설정은 JDBC를 이용한 JdbcBatchItemWriter의 설정 예시이다. JdbcCursorItemReader 설정과 마찬가지로 dataSouce 속성으로 DB connection을 넣어올 수 있는 datasource를 지정하고, sql 속성에 실행할 쿼리를 설정한다.

<bean id="itemWriter" class="org.springframework.batch.item.database.JdbcBatchItemWriter">
	<property name="assertUpdates" value="true" />
	<property name="itemSqlParameterSourceProvider">
		<bean class="org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider" />
	</property>
	<property name="sql" value="UPDATE CUSTOMER set credit = :credit where id = :id" />
	<property name="dataSource" ref="dataSource" />
</bean>

IbatisBatchItemWriter

아래 XML 설정은 iBatis를 이용한 IbatisBatchItemWriter의 설정 예시이다.

<bean id="itemWriter"
	class="org.springframework.batch.item.database.IbatisBatchItemWriter">
	<property name="statementId" value="updateCredit" />
	<property name="sqlMapClient" ref="sqlMapClient" />
</bean>

sqlMapClient의 참조는 아래와 같다. configLocation 속성에 iBatis를 통해 쿼리를 작성해둔 파일의 경로를 설정한다.

<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
	<property name="dataSource" ref="dataSource" />
	<property name="configLocation" value="ibatis-config.xml" />	
</bean>

전자정부에서 제공하는 eGovFlatFileItemWriter

스프링 배치에서 제공하는 파일 기반 관련설정을 사용할 경우, 대용량 데이터 처리 시간이 상용 배치프레임워크 대비 성능이 떨어졌다. 이 문제를 해결하기 위해서 전자정부에서는 파일 ItemWriter의 요소 중 성능저하 요인인 LineAggregator 부분을 개선하여 제공한다

스프링에서 제공하는 BeanWrapperFieldExtractor를 경량화 한 EgovFieldExtractor와 FormatterLineAggreagotor의 기능을 경량화 한 EgovFixedLineAggregator를 제공한다.

스프링 FlatFileItemWriter 구조전자정부 EgovFlatFileItemWriter 구조
imageimage
개선사항설명
EgovFieldExtractor스프링에서 제공하는 BeanWrapperFieldExtractor를 개선하여 item에서 field 값을 추출하는 과정의 성능을 개선한 FieldExtractor 제공
EgovFixedLineAggregator스프링에서 제공하는 FormatterLineAggregator는 Java의 format() 메소드를 이용하여 String을 다양한 format으로 변환할 수 있지만 가장 기본 설정인 문자열 길이만 지정할 때 성능이 떨어지는 단점이 있다.
따라서 사용자가 문자열 길이만 지정할 때를 고려해 경량화하여 성능의 초점을 둔 LineAggregator 제공
(format 지정이 필요한 경우, format을 VO에서 직접 적용하여 FormatterLineAggregator와 같은 기능이지만 성능 개선 된 EgovFixedLineAggregator 사용 가능)

✔ 스프링에서 제공하는 FormatterLineAggregator, DelimitedLineAggregator와 EgovFieldExtractor는 동시 사용이 가능하므로 BeanWrapperFieldExtractor 대신 EgovFieldExtractor 사용 시, 보다 좋은 성능으로 write 할 수 있다.

BeanWrapperFieldExtractor, FormatterLineAggregator(or DelimitedLineAggregator)를 사용한 설정과 EgovFieldExtractor, EgovFixedLineAggregator(or DelimitedLineAggregator)를 사용한 FlatFileItemWriter 설정 비교는 아래와 같다.

Fixed Length(고정길이) 방식 설정

구분설정
스프링 FlatFileItemWriter
<bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step">
   <property name="resource" value="#{jobParameters[outputFile]}" />
   <property name="lineAggregator">
      <bean class="org.springframework.batch.item.file.transform.FormatterLineAggregator">
         <property name="fieldExtractor">
	    <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
		<property name="names" value="name,credit" />
	    </bean>
	 </property>
	 <property name="format" value="%-9s%-2s" />
      </bean>
   </property>
</bean>
전자정부 EgovFlatFileItemWriter
<bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step">
   <property name="resource" value="#{jobParameters[outputFile]}" />
   <property name="lineAggregator">
      <bean class="egovframework.brte.core.item.file.transform.EgovFixedLengthLineAggregator">
         <property name="fieldExtractor">
	    <bean class="egovframework.brte.core.item.file.transform.EgovFieldExtractor">
	       <property name="names" value="name,credit" />
	    </bean>
	 </property>
	 <property name="fieldRanges" value="9,2" />						
      </bean>
   </property>
</bean>
EgovFlatFileItemWriter 설정항목설명
fieldRangesItem의 필드 값들을 1 Line의 String으로 만들 때 필드값의 범위(고정길이) 지정
namesVO 클래스의 필드를 나타낸다.
padding공백 패턴 설정

Delimited(구분자) 방식 설정

구분설정
스프링 FlatFileItemWriter
<bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step">
   <property name="resource" value="#{jobParameters[outputFile]}" />
   <property name="lineAggregator">
      <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
	 <property name="fieldExtractor">
	    <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
	       <property name="names" value="name,credit"/>					
	    </bean>
	 </property>
         <property name="delimiter" value=","/>
      </bean>
    </property>
</bean>
전자정부 EgovFlatFileItemWriter
<bean id="itemWriter" class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step">
   <property name="resource" value="#{jobParameters[outputFile]}" />
   <property name="lineAggregator">
      <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
	 <property name="fieldExtractor">
	    <bean class="egovframework.brte.core.item.file.transform.EgovFieldExtractor">
		<property name="names" value="name,credit"/>					
	    </bean>
	 </property>
         <property name="delimiter" value=","/>
      </bean>
   </property>
</bean>
EgovFlatFileItemWriter 설정항목설명
delimiterItem의 필드 값들을 1 Line의 String으로 만들 때 경계가 되는 구분자 지정
namesVO 클래스의 필드를 나타낸다.

사용하는 LineAggregator에 따라 설정항목이 다르다.

LineAggregator설정항목설명설정 예
DelimitedLineAggregatordelimiter필드의 경계를 구별해주는 문자, (콤마)
FormatterLineAggregatorformat필드의 형식과 필드 경계의 범위%-9s%-2s
EgovFixedLengthLineAggregatorfieldRanges필드 경계의 범위9,2

전자정부에서 제공하는 eGovDBItemWriter

스프링에서 제공하는 JdbcBatchItemWriter는 사용자가 PreparedStatement를 setter하기 위한 클래스를 직접 작성하지 않고, XML 설정시 쿼리의 파라미터값을 지정만으로 자동으로 PreparedStatement를 setter해주는 기능을 제공한다. 하지만, 이 기능을 이용하면 대용량 데이터 처리 시간이 상용 배치프레임워크과 비교하여 큰 차이가 발생한다. 이러한 차이를 개선하고자 전자정부프레임워크에서는 EgovJdbcBatchItemWriter를 제공한다.

스프링 JdbcBatchItemWriter구조전자정부 EgovJdbcBatchItemWriter 구조
imageimage

자동으로 PreparedStatement를 setter 할 경우 JdbcBatchItemWriter는 BeanPropertyItemSqlParameterSourceProvider클래스를 사용하고 EgovJdbcBatchItemWriter는 EgovMethodMapItemPreparedStatementSetter클래스를 사용한다. 설정은 아래와 같다.

구분설정
JdbcBatchItemWriter
	<bean id="itemWriter" class="org.springframework.batch.item.database.JdbcBatchItemWriter">
		<property name="assertUpdates" value="true" />
		<property name="itemSqlParameterSourceProvider">
			<bean class="org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider" />
		</property>
		<property name="sql" value="UPDATE CUSTOMER set credit = :credit where id = :id" />
		<property name="dataSource" ref="dataSource" />
	</bean>
EgovJdbcBatchItemWriter
	<bean id="itemWriter" class="egovframework.brte.core.item.database.EgovJdbcBatchItemWriter">
		<property name="assertUpdates" value="true" />
		<property name="itemPreparedStatementSetter">
			<bean class="egovframework.brte.core.item.database.support.EgovMethodMapItemPreparedStatementSetter" />
		</property>
		<property name="sql" value="UPDATE CUSTOMER set credit =? where id =?"/>
		<property name="params" value="credit,id"/>
		<property name="dataSource" ref="dataSource" />
	</bean>

✔ EgovMethodMapItemPreparedStatementSetter에는 파라미터의 값둘을 params의 value 값으로 설정한다.

보다 자세히 설명하면,

writer설정설명
JdbcBatchItemWriter사용자가 작성한 class사용자가 setValues 메소드를 직접 작성하여 PreparedStatement의 데이터 설정
BeanPropertyItemSqlParameterSourceProvidersql에 파라미터 설정으로 PreparedStatement의 데이터 설정
EgovJdbcBatchItemWriter사용자가 작성한 class사용자가 setValues 메소드를 직접 작성하여 PreparedStatement의 데이터 설정
EgovMethodMapItemPreparedStatementSettersql과 params의 설정으로 PreparedStatement의 데이터 설정

또한 EgovJdbcBatchItemWriter도 사용자가 직접 작성한 class를 PreparedStatementSetter로 설정할 수 있다. 클래스 작성시에는 EgovItemPreparedStatementSetter를 상속하여 사용한다. 아래의 EmployeeItemPreparedStatementSetter 클래스는 EgovItemPreparedStatementSetter를 상속받아서 사용자가 직접 작성한 것이다.

<bean id="itemWriter" class="org.springframework.batch.item.database.EgovJdbcBatchItemWriter">
	<property name="itemPreparedStatementSetter">
		<bean class="egovframework.brte.sample.example.support.EmployeeItemPreparedStatementSetter" />
	</property>
	<property  name="sql“  value="update into UIP_EMPLOYEE (num, name, sex) values (?, ?, ?)" />
	<property name="dataSource" ref="dataSource" />
</bean>

전자정부에서 제공하는 eGovIndexFileWriter

배치 Job 정의 시 Resource 엘리먼트의 shell step에 shell script에 포함된 파일명에서 일련번호(index)를 사용할 수 있는 Writer를 제공한다. Index 파일명을 사용하면 파일의 일련번호를 기준으로 동적인 파일명 생성이 가능하다.

Index(NDX) 파일명 치환 로직

NDX File : 파일 이름이 “[이름]_NDX_[YYYYMMDDhhmmss]” 형식으로 이루어진 파일
           ex) Sample_NDX_20121126151237

NDX 일련번호 : 파일명 끝에 14자리 수의 생성시간(년월일시분초)

NDX 파일명 치환 : “[이름]_NDX(Index)” 형식의 파일명은 해당 디렉터리의 NDX 파일에 대해 Index에 해당하는 실제 파일명으로 치환됨

(-2 ) : 일련번호 기준 마지막 파일에서 두 번째 이전 파일명으로 치환됨
(-1 ) : 일련번호 기준 마지막 파일에서 첫 번째 이전 파일명으로 치환됨
( 0 ) : 일련번호 기준 마지막 파일명으로 치환됨
(+1 ) : 일련번호 기준 마지막 파일에서 Index를 1 증가시켜 새로운 파일 생성

NDX 파일목록 중 잘못된 파일명이 존재할 경우 에러를 발생한다.

구분예시비고
에러Sample_NDX_20180104index 자릿수가 10자리
에러Sample_NDX_000Aindex는 숫자만 허용함
무시Sample_20180104123456NDX 파일이 아닌 일반 파일로 인식

Index(NDX) Writer 방식 설정

Index Reader을 통해 읽어드린 파일을 NDX파일 설정에 따라 동적으로 새로운 파일을 생성한다.

<bean id="fileIndex-delimitedItemWriter" class="egovframework.rte.bat.core.item.file.EgovIndexFileWriter" scope="step">
	<property name="indexResource" value="file:./src/main/resources/egovframework/batch/data/inputs/csvData_NDX(+1)" />
	<property name="lineAggregator">
		<bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
			<property name="delimiter" value="," />
			<property name="fieldExtractor">
				<bean class="egovframework.rte.bat.core.item.file.transform.EgovFieldExtractor">
					<property name="names" value="name,credit" />
				</bean>
			</property>
		</bean>
	</property>
</bean>
EgovIndexFileWriter 설정항목내용예시
indexResource설정된 index(NDX)에 따른 동적인 파일 생성Index(NDX) 파일명 치환 로직 참조

전자정부에서 제공하는 EgovMyBatisBatchItemWriter

배치 처리시 mybatis에서 데이터를 쓰기 위해 EgovMyBatisBatchItemWriter 서비스를 제공합니다.(mybatis MyBatisBatchItemWriter 클래스를 확장한 서비스) 실행환경 제공 Resource Variable, Step Variable, Job Variable 서비스와 함께 사용 가능하다.

image

EgovMyBatisBatchItemWriter 설정항목

설정항목내용예시
sqlSessionFactoryreader에 별도로 구현한 sessionFactorysqlSession
statementId네임스페이스를 가진 매퍼 파일을 Query IdEmpMapper.selectEmpList
resourceVariable표준프레임워크 실행환경 Resource Variable 서비스를 사용하기 위한 설정resourceVariable
jobVariable표준프레임워크 실행환경 Step Variable 서비스를 사용하기 위한 설정jobVariable
stepVariable표준프레임워크 실행환경 Job Variable 서비스를 사용하기 위한 설정stepVariable

EgovMyBatisBatchItemWriter 설정항목 설정

<bean id="mybatisJobStep.mybatisItemWriter" class="egovframework.rte.bat.item.database.EgovMyBatisBatchItemWriter">
	<property name="resourceVariable" ref="resourceVariable" />
	<property name="jobVariable" ref="jobVariable" />
	<property name="stepVariable" ref="stepVariable" />
	<property name="sqlSessionFactory" ref="sqlSession" />
	<property name="statementId" value="EmpMapper.updateEmp" />
</bean>

참고자료

7.10 - Resource Variable

사용자 정의 리소스 변수 선언 후 Setp에서 ItemReader, ItemWriter에서 사용자 정의 리소스를 사용할 수 있도록 EgovResourceVariable를 통해서 지원한다.

Resource Variable

개요

사용자 정의 리소스 변수 선언 후 Setp에서 ItemReader, ItemWriter에서 사용자 정의 리소스를 사용할 수 있도록 EgovResourceVariable를 통해서 지원한다.

image

설명

EgovResourceVariable 설정

배치실행환경에서 제공하는 EgovResourceVariable 사용하여 사용자 정의 리소스를 설정한다.

<bean id="egovResourceVariable" class="egovframework.rte.bat.support.EgovResourceVariable">
	<property name="pros">
	<props>
		<prop key="input.resource">file:./src/main/resources/egovframework/batch/data/inputs/csvData.csv</prop>
		<prop key="writer.resource">file:./target/test-outputs/csvOutput_ResourceVariable_#{new java.text.SimpleDateFormat('yyyyMMddHHmmssSS').format(new java.util.Date())}.csv</prop>
	</props>
	</property>
</bean>

step 정의 시 리소스 사용

Setp에서 ItemReader, ItemWriter 사용시 사용자 정의 리소스 변수를 사용하여 resource 설정이 가능하다.

<bean id="delimitedToDelimitedJob-ResourceVariable.delimitedToDelimitedStep.delimitedItemReader"
	class="org.springframework.batch.item.file.FlatFileItemReader" scope="step">
	<property name="resource" value="#{egovResourceVariable.getVariable('input.resource')}" />
	<property name="lineMapper">
		<bean class="egovframework.rte.bat.core.item.file.mapping.EgovDefaultLineMapper">
			<property name="lineTokenizer">
				<bean class="egovframework.rte.bat.core.item.file.transform.EgovDelimitedLineTokenizer">
					<property name="delimiter" value="," />
				</bean>
			</property>
			<property name="objectMapper">
				<bean class="egovframework.rte.bat.core.item.file.mapping.EgovObjectMapper">
					<property name="type"
						value="egovframework.example.bat.domain.trade.CustomerCredit" />
					<property name="names" value="name,credit" />
				</bean>
			</property>
		</bean>
	</property>
</bean>
 
<bean id="delimitedToDelimitedJob-ResourceVariable.delimitedToDelimitedStep.delimitedItemWriter"
	class="org.springframework.batch.item.file.FlatFileItemWriter" scope="step">
	<property name="resource" value="#{egovResourceVariable.getVariable('writer.resource')}" />
	<property name="lineAggregator">
		<bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
			<property name="delimiter" value="," />
			<property name="fieldExtractor">
				<bean class="egovframework.rte.bat.core.item.file.transform.EgovFieldExtractor">
					<property name="names" value="name,credit" />
				</bean>
			</property>
		</bean>
	</property>
</bean>

7.11 - JobRepository

JobRepository는 배치 작업 중의 정보를 저장하는 역할을 한다. 어떠한 Job이 언제 수행되었고, 언제 끝났으며, 몇 번이 실행되었고 실행에 대한 결과가 어떤지 등의 배치 작업의 수행과 관련된 모든 meta data가 저장되어 있다.

JobRepository

개요

JobRepository는 배치 작업 중의 정보를 저장하는 역할을 한다. 어떠한 Job이 언제 수행되었고, 언제 끝났으며, 몇 번이 실행되었고 실행에 대한 결과가 어떤지 등의 배치 작업의 수행과 관련된 모든 meta data가 저장되어 있다.

설명

JobRepository은 수행되는 Job에 대한 정보를 담고 있는 저장소로 배치작업의 지속성 메커니즘이다. JobRepository는 Spring Batch에서 JobExecution와 StepExecution 등과 같은 지속성을 가진 정보의 기본 CRUD작업에 사용된다. 배치작업이 처음 실행되면 JobRepository에서 JobExecution이 생성되고 배치작업이 실행되는 동안 StepExecution 및 JobExecution의 정보들이 JobRepository에 저장되고 갱신되어 지속된다.

JobRepository는 배치 네임 스페이스를 통해서나 JobRepositoryFactoryBean 클래스를 사용하여 아래와 같이 설정할 수 있다.

<job-repository id="jobRepository"
    data-source="dataSource"
    transaction-manager="transactionManager"
    isolation-level-for-create="SERIALIZABLE"
    table-prefix="BATCH_"
    max-varchar-length="1000"
/>
<bean id="jobRepository" class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean"
		p:dataSource-ref="dataSource" p:transactionManager-ref="transactionManager" p:isolation-level-for-create="ISOLATION_DEFAULT"
	       p:table-prefix="BATCH_" p:max-var-char-length="1000"/>

트랜잭션 설정(Transaction Configuration for the JobRepository)

네임 스페이스를 사용하는 경우, transactional advice가 자동으로 Repository 주위에 생성된다. 배치작업의 실패 후 다시 시작할 필요있는가의 상태를 포함하고 있는 메타 데이터가 제대로 지속되어 있는지 확인한다. repository이 트랜잭션 처리를 하지 않는다면 프레임 워크의 처리가 잘 정의되지 않는다. 기본적인 isolation level은 가장 격리 수준이 높은 serializable이고 재정의도 가능하다.

<job-repository id="jobRepository"
                isolation-level-for-create="REPEATABLE_READ" />

네임스페이스나 Bean 정의를 사용하지 않는 경우에는 AOP를 이용하여 Repository에 대한 트랜잭션의 관리할 수 있도록 설정해야 한다.

<aop:config>
    <aop:advisor 
           pointcut="execution(* org.springframework.batch.core..*Repository+.*(..))"/>
    <advice-ref="txAdvice" />
</aop:config>
 
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="*" />
    </tx:attributes>
</tx:advice>

위의 설정은 거의 변경없이 사용할 수 있다.다만, 네임 스페이스의 선언을 포함하고 있는지 Spring-aop, Spring-tx이 classpath에 있는지 확인하여야 한다.

테이블접두사 변경(Changing the Table Prefix)

JobRepository는 메타 데이터 테이블의 테이블 접두사 수정도 가능하다. 기본적으로 모든 데이터 테이블은 BATCH_JOB_EXECUTION 와 BATCH_STEP_EXECUTION 등과 같이 BATCH_로 시작한다.하지만 테이블명 앞에 스키마명을 추가하거나, 같은 스키마 내에서 메타 데이터 테이블의 하나 이상의 세트가 필요하면 테이블 접두사 수정이 필요하다.

<job-repository id="jobRepository"
                table-prefix="SYSTEM.TEST_" />

위와 같이 설정한다면 모든 쿼리에 메타 데이터 테이블명이 SYSTEM.TEST_”로 시작된다. BATCH_JOB_EXECUTION은 SYSTEM.TEST_JOB_EXECUTION으로 변경될 것이다.

✔오직 테이블의 접두사만 변경가능하다. 테이블명과 컬럼명은 수정할 수 없다.

메모리 Repository(In-Memory Repository)

Spring 배치는 jobRepository를 데이터베이스가 아닌 메모리로 설정할 수 있다. 작업에 대한 상태를 유지하지 않아도 되는 배치작업의 도메인 개체를 데이터 베이스에 저장할 경우, 각각의 커밋 시점에 추가 시간이 걸린다. 이 경우, 메모리 Repository를 통해 잡을 실행한다.

<bean id="jobRepository" 
  class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean">
    <property name="transactionManager" ref="transactionManager"/>
</bean>

메모리는 JVM 인스턴스가 다시 시작하는 것을 허용하지 않으며, 휘발성을 가진 jobRepository이다. 또한 동일한 매개 변수 두 작업 인스턴스가 동시에 실행되는 것을 보장 할 수 없다. multi-threaded 배치작업이나 파티션 작업에 적합하지 않을 수 있다.그러므로 JobRepository는 데이터베이스을 사용하여야 한다.

하지만 repository내에서 rollback이 있으므로 트랜잭션 manager의 설정이 필요하다.그리고 테스트를 위해 많은 사람들이 비즈니스 논리상 여전히 트랜잭션이 있기 때문에 ResourcelessTransactionManager가 유용하다.

비표준 데이터 베이스 타입(Non-standard Database Types in a Repository)

Spring Batch에서 지원되는 데이터베이스 목록이외의 데이터베이스를 사용하는 경우,비슷하게 지원하는 하는 데이터베이스가 있다면 그것을 사용할 수 있다. 이 작업을 수행하려면 네임 스페이스 사용하는 대신에 JobRepositoryFactoryBean를 사용하여 가장 가깝게 일치하는 데이터베이스 유형을 설정 할 수 있다

  • Spring Batch에서 지원되는 데이터베이스 타입 : DERBY, DB2, DB2ZOS, HSQL, SQLSERVER, MYSQL, ORACLE, POSTGRES, SYBASE, H2
<bean id="jobRepository" class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean">
    <property name="databaseType" value="db2"/>
    <property name="dataSource" ref="dataSource"/>
</bean>

주의

altibasetibero는 지원되는 데이터베이스 타입이 아니다. altibase나 tibero 연결시에는 jobRepository에 databaseType으로 oracle을 추가 설정해야 한다.

<bean id="jobRepository"
		class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean"
		p:dataSource-ref="dataSource" p:databaseType="oracle" p:transactionManager-ref="transactionManager" p:lobHandler-ref="lobHandler"/>

참고자료

  • JobRepository :http://static.springsource.org/spring-batch/reference/html/configureJob.html#configuringJobRepository
  • declarative_transaction_management
  • Meta-Data Schema :http://static.springsource.org/spring-batch/reference/html/metaDataSchema.html

7.12 - JobLauncher

JobLauncher는 배치작업을 실행시키는 역할을 한다. Job과 Job Parameters를 이용하여 요청된 배치 작업을 수행한 후 JobExecution을 반환한다.

JobLauncher

개요

JobLauncher는 배치작업을 실행시키는 역할을 한다. Job과 Job Parameters를 이용하여 요청된 배치 작업을 수행한 후 JobExecution을 반환한다.

설명

JobLauncher 인터페이스를 보면 Job과 Job Parameter를 이용하여 요청된 Job을 수행한 후 JobExecution을 반환되는 run메소드가 정의되어 있다.

public interface JobLauncher {
 
	public JobExecution run(Job job, JobParameters jobParameters) throws JobExecutionAlreadyRunningException,
			JobRestartException, JobInstanceAlreadyCompleteException, JobParametersInvalidException;
 
}

JobLauncher 인터페이스의 기본 구현 클래스로는 SimpleJobLauncher이 제공된다. SimpleJobLauncher클래스는 JobName과 JobParameter를 이용하여 JobRepository에서 Job의 실행시도를 나타내는 JobExecution을 획득하고 작업을 수행한다.

이를 이용한 jobLauncher 설정은 아래와 같다. JobExecution을 획득하기 위한 jobRepository의 설정이 필수이다.

<bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
       <property name="jobRepository" ref="jobRepository" />
</bean>

JobLauncher는 taskExecutor 설정을 통해 Job을 동기적, 혹은 비동기적으로 실행할 수 있다. 별도로 설정하지 않으면 syncTaskExecutor클래스가 디폴트로 설정되어 동기적으로 아래와 같이 실행된다. client에게 배치작업의 요청을 받게 되면 JobLauncher는 하나의 JobExecution을 획득하고, 그것을 배치작업을 실행하는 메소드에 전달하여 최종적으로 배치 작업 후 Client에게 JobExecution을 반환한다.

  • 동기

image

위의 흐름은 간단하며 스케줄러에서 실행하면 잘 동작하지만, HTTP 요청에서 시작하려고 할 때 문제가 발생한다. 배치 작업의 특성상 처리시간이 오래 걸리는 작업이 많을 것이고,그 작업시간동안 HTTP 응답을 계속 기다리는 것은 좋지 않다. 이 경우에는 아래와 같이 SimpleJobLauncher가 Client에게 즉시 JobExecution을 반환하는 비동기식 동작 방법이 필요하다.

  • 비동기

image

JobLauncher 설정에서 SimpleAsyncTaskExecutor클래스를 통해 비동기로도 쉽게 설정할 수 있다.

<bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
       <property name="jobRepository" ref="jobRepository" />
       <property name="taskExecutor">
                <bean class="org.springframework.core.task.SimpleAsyncTaskExecutor" />
       </property>
</bean>

Spring의 TaskExecutor 인터페이스의 모든 구현은 비동기로 실행하는 배치작업에 대한 제어의 목적으로 사용된다.

참고자료

7.13 - Remote JobLauncher

Remote JobLauncher는 온라인에서 배치 서버의 Batch Job 작업을 실행하며, 클라이언트와 서버 간 요청된 배치 작업을 수행한다. 이를 위해 Hessian Binary Web Service를 사용해 간편하게 웹서비스를 구현하며, Spring의 HessianProxyFactoryBean과 HessianServiceExporter를 통해 편리한 통합을 지원한다.

Remote JobLauncher

개요

Remote JobLauncher는 온라인 상에서 별도의 배치 서버의 Batch Job작업을 실행시키는 역할을 한다. 온라인 상의 Client와 Server를 이용하여 요청된 배치 작업을 수행한다.

구성

온라인상의 Remote JobLauncher를 구현하기 위하여 Hessian Binary Web Service를 사용한다. Hessian 웹서비스는 별도의 대형 프레임워크를 설치하지 않고도 간편하게 사용할 수 있은 웹서비스이며, HTTP기반의 경량 바이러리 프로토콜로 별도의 확장없이 바이너리 데이터를 전송하는데 적합하다. 또한, 스피링의 HessianProxyFactoryBean과 HessianServiceExporter를 사용하여 편리한 Integration을 지원한다.

Hessian을 사용하기 위하여 아래와 같이 라이브러리 디펜던시를 설정한다.

<dependency>
    <groupId>com.caucho</groupId>
    <artifactId>hessian</artifactId>
    <version>4.0.38</version>
</dependency>

아래의 예제는 표준프레임워크 개발환경(v3.7)의 배치 템플릿 (SAM파일 Web 기반)을 사용하여 기존의 JobLauncher를 웹서비스에 등록하여 외부(Client)에서 호출하여 실행한다. 본 가이드의 실행 예제는 RemoteJobLauncher 예제에서 다운로드하여 확인할 수 있다.

Remote JobLauncher의 예제 작성 방법을 다음과 같다.

Web 서비스 (Server)

  • (Step 1) 표준프레임워크 개발환경에서 배치 템플릿을 생성한다. 템플릿의 대상은 SAM파일 형식의 Web 프로젝트이다. 관련 템플릿 생성 방법은 배치 템플릿 위저드를 참조한다.
  • (Step 2) 생성된 배치 템플릿에 Hessain Web Service의 라이브러리를 등록한다.
  • (Step 3) 서버의 RemoteJobLauncher를 작성한다. (interface 및 Implement 클래스 작성) RemoteJobLauncher의 구현체는 Template의 BatchRunController의 batchRun을 참조한다.
  • (Step 4) 작성된 RemoteJobLauncher를 스프링의 HessianServletExporter을 이용하여 빈생성과 새로운 Servlet을 작성한다.

테스트 (Client)

  • (Step 1) Client에서는 HessianProxyFactoryBean을 사용하여 서버의 웹서비스 Url을 등록한다.
  • (Step 2) 등록된 웹서비스를 통하여 RemoteJobLauncher를 실행한다.

image

설명

Remote 웹 서비스 (Sever-side)

Step 1 개발환경의 배치 템플릿 생성 개발환경의 “New Batch Template Project”를 이용하여 SAM파일 형식의 Web 기반 템플릿을 생성한다. 배치 템플릿 관련 자세한 사항은 배치 템플릿 위저드를 참조한다.

Step 2 Hessain Web Service의 라이브러리 등록 작성할 Batch JobLauncher를 온라인상에서 실행하기 위하여 바이너리 형식의 간편한 Web Service인 Hessian을 사용하며, 해당 라이브러리를 pom.xml에 등록한다.

<dependency>
    <groupId>com.caucho</groupId>
    <artifactId>hessian</artifactId>
    <version>4.0.38</version>
</dependency>

Step 3 RemoteJobLaucher 작성 RemoteJobLauncher는 웹서비스에 호출할 수 있는 인터페이스이며 이를 통하여 작성된 배치를 실행하고, 또한 필요한 경우 결과값을 넘겨주는 역활을 한다. 작성된 RemoteJobLauncher를 HessianServiceExporter에 등록하기 위하여 인터페이스 클래스와 구현체(Implement)클래스를 작성하여야 한다.

package egovframework.example.bat.remote;
 
import java.util.HashMap;
 
public interface RemoteJobLauncher {
 
	public HashMap<String,Object> callRemoteBatchRunner(String jobName);	
 
}
package egovframework.example.bat.remote;
 
import java.util.ArrayList;
...
import egovframework.rte.bat.core.launch.support.EgovBatchRunner;
 
public class RemoteJobLauncherImpl implements RemoteJobLauncher {
 
	...
 
	@Override
	public HashMap<String,Object> callRemoteBatchRunner(String jobName) {
 
        //배치 템플릿의 BatchRunController.java의 batchRun메소드를 참조한다.
        ...
 
        return resultMap;
        }
  • callRemoteBatchRunner메소드는 배치 템플릿의 BatchRunController클래스의 batchRun메소드를 참조하여 작성한다.

Step 4 RemoteJobLauncher로 빈생성 및 서블릿 등록

  • 빈등록을 위하여 HessianServiceExporter를 사용하며, 인자값으로 service와 serviceInterface를 작성한다. HassianServiceExporter의 name(EgovJobLauncher.remote)은 요청 Url 서비스의 매핑 주소이며, 별도의 명시적 매핑 핸들러를 지정하지 않고도 DispatcherServlet의 매핑에 해당 Bean의 이름을 통하여 서비스를 제공한다.
<bean name="/EgovJobLauncher.remote" class="org.springframework.remoting.caucho.HessianServiceExporter">
	<property name="service" ref="remoteJobLauncher" />
	<property name="serviceInterface" value="egovframework.example.bat.remote.RemoteJobLauncher" />
</bean>
<bean id="remoteJobLauncher" class="egovframework.example.bat.remote.RemoteJobLauncherImpl" />
** 웹서비스 호출 예시 **
http://<domain_address>/<context_path>/EgovJobLauncher.remote
  • 서블릿 설정은 web.xml에 별도로 작성한다. 본 가이드에서는 *.remote사용하여 별도의 url-pattern을 작성하였다.
<servlet>
	<servlet-name>remoting</servlet-name>
	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
	<init-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/config/egovframework/springmvc/hessian-servlet.xml</param-value>
	</init-param>
	<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
	<servlet-name>remoting</servlet-name>
	<url-pattern>*.remote</url-pattern>
</servlet-mapping>

테스트 (Client-side)

Remote 배치 서비스를 호출하기 위하여 스프링의 HessianProxyFactoryBean을 사용하며 배치 실행 후 결과 값으로 배치 실행정보를 출력한다.

  • 본 테스트 예제는 프로젝트의 src/test영역에 자바 클래스(RemoteJobClient_test)와 빈설정(client-beans-remote)파일로 구성되어 있다.

Step 1 HessainProxyFactoryBean에 RemoteJobLanucher를 등록한다. 인자값으로 serviceUrl을 지정할 수 있으며, serviceInterface를 지정하여 런처의 메소드를 실행할 수 있다.

<bean id="remoteHessianJobLauncher" 
      class="org.springframework.remoting.caucho.HessianProxyFactoryBean">		
	<property name="serviceUrl"
                  value="http://localhost:8080/egovframework.example.bat.template.sam.web/EgovJobLauncher.remote" />
	<property name="serviceInterface" value="egovframework.example.bat.remote.RemoteJobLauncher" />
</bean>

Step 2 테스트 파일을 작성하여 등록된 웹서비스를 호출한다. 아래와 같이 테스트 파일을 작성하여 웹서비스를 호출하며 템플릿 예제의 “delimitedToDelimitedJob” 배치 job을 실행하여 결과로 Job Instance 및 step Info를 출력한다.

package egovtest.webBatchRemote;
 
import org.springframework.context.ApplicationContext;
...
import egovframework.example.bat.remote.RemoteJobLauncher;
 
public class RemoteJobClient_test {
 
public static void main(String[] args) {
		ApplicationContext context = new ClassPathXmlApplicationContext("client-beans-remote.xml");
		RemoteJobLauncher remoteJobLauncher = (RemoteJobLauncher) context.getBean("remoteHessianJobLauncher");
		String jobName = "delimitedToDelimitedJob";
 
		HashMap<String,Object> map = (HashMap<String, Object>)remoteJobLauncher.callRemoteBatchRunner(jobName);
 
		HashMap<String, Object> jobInstances = (HashMap<String, Object>) map.get("jobInstances");
		List<HashMap<String, Object>> stepsInfo = (ArrayList<HashMap<String, Object>>)map.get("stepsInfo");
 
		System.out.println("[jobInstance] ===============================================================");
		System.out.println("job id = "+jobInstances.get("id"));
		System.out.println("job name = "+jobInstances.get("name"));
		System.out.println("parameters = "+jobInstances.get("parameters"));
		System.out.println("startTime = "+jobInstances.get("startTime"));
		System.out.println("endTime = "+jobInstances.get("endTime"));
		System.out.println("isRunning = "+ ((boolean)jobInstances.get("isRunning") ? "Running" : "Ready"));
		System.out.println("exitStatus = "+jobInstances.get("exitStatus"));
 
		System.out.println("[stepsInfo] ===============================================================");
		for(HashMap<String,Object> stepInfo : stepsInfo) {
			System.out.println("stepId = "+stepInfo.get("stepId"));
			System.out.println("stepName = "+stepInfo.get("stepName"));
			System.out.println("readCount = "+stepInfo.get("readCount"));
			System.out.println("writeCount = "+stepInfo.get("writeCount"));
			System.out.println("readSkipCount = "+stepInfo.get("readSkipCount"));
			System.out.println("processSkipCount = "+stepInfo.get("processSkipCount"));
			System.out.println("writeSkipCount = "+stepInfo.get("writeSkipCount"));
			System.out.println("totalSkipCount = "+stepInfo.get("totalSkipCount"));
			System.out.println("commitCount = "+stepInfo.get("commitCount"));
			System.out.println("rollbackCount = "+stepInfo.get("rollbackCount"));
			System.out.println("exitStatus = "+stepInfo.get("exitStatus"));
		}
	}
}

출력 결과

[jobInstance] ===============================================================
job id = 0
job name = delimitedToDelimitedJob
parameters = {inputFile=classpath:/egovframework/batch/data/inputs/csvData.csv, outputFile=file:xxxxx/remote-batch-output/csvOutput_xxxx.csv, timestamp=xxxxxxxxxxx}
startTime = 201x-xx-xx xx:xx:xx.xxx
endTime = 201x-xx-xx xx:xx:xx.xxx
isRunning = Ready
exitStatus = COMPLETED
[stepsInfo] ===============================================================
stepId = 0
stepName = delimitedToDelimitedStep
readCount = 4
writeCount = 4
readSkipCount = 0
processSkipCount = 0
writeSkipCount = 0
totalSkipCount = 0
commitCount = 3
rollbackCount = 0
exitStatus = COMPLETED

참고자료

7.14 - JobRunner

JobRunner는 외부 실행 모듈과 JobLauncher를 연결해주는 모듈로, 용도에 맞게 구현이 필요하다. 전자정부 표준프레임워크에서는 작업실행 유형에 따라 미리 JobRunner를 미리 구현한 표준 Batch Runner를 제공한다.

JobRunner

개요

JobRunner는 외부 실행 모듈과 JobLauncher를 연결해주는 모듈로, 용도에 맞게 구현이 필요하다. 전자정부 표준프레임워크에서는 작업실행 유형에 따라 미리 JobRunner를 미리 구현한 표준 Batch Runner를 제공한다.

설명

배치작업의 실행 유형에 따라 아래와 같이 3가지의 Batch Runner를 제공한다.

  • EgovBatchRunner: Web, Java Application 등을 이용하여 범용적으로 실행되는 일괄처리 작업에 사용한다.
  • EgovCommandLineRunner: 외부 프로그램(Windows: / Unix/Linux: crontab 등)이나 명령 프롬프트(Windows: bat / Unix/Linux: Shell)에서 독립적으로 실행되는 배치작업에 사용한다.
  • EgovSchedulerRunner: 주기적으로 실행되어야 하는 일괄처리 작업에 사용한다.

각 Batch Runner가 제공하는 기능은 아래와 같다.

Batch Runner 종류Java Application 실행Web 실행Job 상태 모니터링Scheduling 기능명령 프롬프트 연동 지원
EgovBatchRunnerOOOX
EgovCommandLineRunnerOXOXO
EgovSchedulerRunnerOOXO

✔ EgovBatchRunner, EgovSchedulerRunner에서 명령 프롬프트 연동을 위해서는 추가적인 구현이 필요하다.

EgovBatchRunner

구조

EgovBatchRunner를 이용하여 Job Operator 및 Job Explorer를 이용하여 Job Config에 등록된 Job을 실행하고, 실행 상태를 변경할 수 있다. 또한 Job Repository에 접근할 수 있는 기능을 제공한다.

image

설정방법

EgovBatchRunner를 사용하기 위해서는 XML 파일에 JobOperator, JobExplorer 그리고 JobRepository가 정의되어야 한다. 그리고 JobOperator를 생성하기 위해서는 JobLauncher와 JobRegistry의 정의가 필요하다.

<bean id="jobOperator"
	class="org.springframework.batch.core.launch.support.SimpleJobOperator"
	p:jobLauncher-ref="jobLauncher" p:jobExplorer-ref="jobExplorer"
	p:jobRepository-ref="jobRepository" p:jobRegistry-ref="jobRegistry" />
 
<bean id="jobLauncher"
	class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
	<property name="jobRepository" ref="jobRepository" />
</bean>
 
<bean id="jobRepository"
	class="org.springframework.batch.core.repository.support.JobRepositoryFactoryBean"
	p:dataSource-ref="egov.dataSource" p:transactionManager-ref="transactionManager"
	p:lobHandler-ref="lobHandler" />
 
<bean id="jobExplorer"
	class="org.springframework.batch.core.explore.support.JobExplorerFactoryBean"
	p:dataSource-ref="egov.dataSource" />
 
<bean id="jobRegistry"
	class="org.springframework.batch.core.configuration.support.MapJobRegistry" />

EgovBatchRunner의 생성자에 JobOperator, JobExplorer, JobRepository를 전달한다.

<bean id="jobBatchRunner" class="egovframework.brte.core.launch.support.EgovBatchRunner">
	<constructor-arg ref="jobOperator" />
	<constructor-arg ref="jobExplorer" />
	<constructor-arg ref="jobRepository" />
</bean>

제공기능

EgovBatchRunner에서는 Job Operator, Job Explorer에서 제공하는 주요 메소드를 기반으로 하여 Job Config에 등록된 Job 이름 조회, Job 시작, 재시작, 정지 등의 기능을 제공한다. Job Repository는 메소드를 직접 제공하지 않는 대신, 필요에 따라 Job Repository 객체에 접근할 수 있도록 하였다.

메소드명설명파라미터
start각각 문자열 형태의 Job 이름, Job Parameter를 이용하여 Job을 시작한다. 이 때, Job Parameter는 기존에 실행했던 Job Parameter와 다른 고유한 값을 가져야 한다.Job 이름, Job Parameter
restartJob의 Execution ID를 이용하여, 정지되었거나 이미 종료 된 Job 중 재실행 가능한 Job 을 재시작한다.Job의 Execution ID
stopJob의 Execution ID를 이용하여, 실행 중인 Job을 정지시킨다.Job의 Execution ID

Job Parameter의 생성

EgovBatchRunner에서 사용할 Job Parameter를 생성하기 위해서 제공하는 메소드로, Job Parameter를 문자열 형태로 생성한다.

메소드명설명파라미터
createUniqueJobParametersTimestamp를 이용하여 유일한 값을 지니는 Job Parameter 문자열을 생성한다.없음
addJobParameter이미 생성된 Job Parameter 문자열에 Job Parameter 문자열을 추가한다.기존 Job Parameter 변수 ,추가할 Job Parameter 이름(키), 값 ,

Job Parameter 생성 예제는 아래와 같다.

  1. Job Parameter를 생성한다. 이 때, 생성되는 Job Parameter는 Timestamp 값을 지니고 있어 유일성이 보장된다.
  2. 생성한 Job Parameter 변수에 “inputFile”이라는 Job Parameter 이름을 지닌 Job Parameter를 추가하고, ”/egovframework/batch/data/inputs/csvData.csv”라는 값을 할당한다.
	String jobParameters = egovBatchRunner.createUniqueJobParameters();
 
	jobParameters = egovBatchRunner.addJobParameter(jobParameters, "inputFile", "/egovframework/batch/data/inputs/csvData.csv");

이렇게 생성된 JobParameters는 XML에서 사용할 수 있다. 자세한 내용은 Job Parameters 항목을 참조한다.

사용방법

EgovBatchRunner 예제

EgovCommandLineRunner

구조

EgovCommandLineRunner는 Job Launcher 및 Job Explorer를 이용하여 Job Config에 등록된 Job을 실행할 수 있으며, 실행할 수 있도록 하는 기능을 제공한다.

image

제공기능

EgovCommandLineRunner에서는 start 메소드를 이용하여 Job을 시작한다. start 메소드에 필요한 파라미터는 아래와 같으며, 배치실행을 위해서는 Job Path와 Job Identifier는 반드시 필요하다.

파라미터설명
Job PathJob 실행에 필요한 context 정보가 들어있는 xml
Job Identifier실행할 Job의 이름, 혹은 Job의 Execution ID
ParametersJob Parameter
Option실행옵션

실행옵션을 지정하지 않았을 경우 Job을 시작한다. 그리고 실행옵션을 지정했을 경우 해당하는 동작을 수행하며, Job Identifier의 지정 방식도 달라진다. 실행옵션의 종류 및 Job Identifier 지정방식은 아래와 같다.

실행옵션설명Job Identifier
미지정Job을 시작한다.Job의 이름
-restart정지되었거나 이미 종료 된 Job 중 재실행 가능한 Job 을 재시작한다.Job의 Execution ID
-stop실행 중인 Job을 정지시킨다.Job의 Execution ID
-nextJob을 JobParameter만 변경하여 실행한다.Job의 Execution ID
-abandon정지된 Job의 상태를 “ABANDONED”으로 변경한다.Job의 Execution ID

사용방법

배치 템플릿을 이용한 EgovCommandLineRunner 예제

EgovSchedulerRunner

구조

기존의 Batch Runner와는 다르게, EgovSchedulerRunner는 Job을 직접 실행하는 것이 아니라 Scheduler를 실행한다. 이 Scheduler가 설정되어 있는 시간 및 주기 간격으로 Job을 실행하게 된다. Scheduler는 Quartz를 사용하고 있으며, Quartz의 자세한 사용법 및 설정 방법은 Scheduling 서비스를 참고한다.

image

설정 및 사용방법

배치 템플릿을 이용한 EgovSchedulerRunner 예제

7.15 - JobRegistry

JobRegistry는 생성된 Job을 자동으로 Map형태로 저장하여 관리한다. JobRegistry는 context에서 Job을 추적하거나 다른 곳에서 생성된 Job을 application context의 중앙에 모을 때 유용하게 사용할 수 있다. 등록된 Job의 이름과 속성들을 조작할 수 있으며, job name과 job instance의 Map의 형태로 이루워져 있다.

JobRegistry

개요

JobRegistry는 생성된 Job을 자동으로 Map형태로 저장하여 관리(추가, 삭제 등)한다.

설명

JobRegistry는 필수는 아니지만 context에서 Job을 추적하거나 다른 곳에서 생성된 Job을 application context의 중앙에 모을 때 유용하다. 등록된 Job의 이름과 속성들을 조작할 수 있으며 job name과 job instance의 Map의 형태로 이루워져 있다.

<bean id="jobRegistry" class="org.springframework.batch.core.configuration.support.MapJobRegistry" />

JobRegistry에 Job을 자동으로 등록하는 방법은 두 가지가 있다.

  • JobRegistryBeanPostProcessor 사용
  • AutomaticJobRegistrar 사용

JobRegistryBeanPostProcessor

이것은 Bean post-processor으로 Application Context가 올라가면서 bean 등록 시, 자동으로 JobRegistry에 Job을 등록 시켜준다.

<bean id="jobRegistryBeanPostProcessor" class="org.springframework.batch.core.configuration.support.JobRegistryBeanPostProcessor">
    <property name="jobRegistry" ref="jobRegistry"/>
</bean>

AutomaticJobRegistrar

부모 context에서 자식 contexts에 생성된 Job들을 사용하기 위해 자동으로 부모 context의 JobRegistry에 Job들을 등록시킨다. AutomaticJobRegistrar을 사용하기 위해서는 아래와 같이 ApplicationContextFactory와 JobLoader의 설정은 필수이다. ApplicationContextFactory는 ClassPathXmlApplicationContextFactory를 이용하여 자식 context를 생성하고 JobLoader는 자식 context를 관리하고 JobRegistry에 Job 등록을 한다.

<bean class="org.spr...AutomaticJobRegistrar">
   <property name="applicationContextFactories">
      <bean class="org.spr...ClasspathXmlApplicationContextsFactoryBean">
         <property name="resources" value="classpath*:/config/job*.xml" />
      </bean>
   </property>
   <property name="jobLoader">
      <bean class="org.spr...DefaultJobLoader">
         <property name="jobRegistry" ref="jobRegistry" />
      </bean>
   </property>
</bean>

참고자료

7.16 - JobExplorer

JobExplorer는 실행 중인 Job 및 Step을 검색하기 위한 시작지점으로서, Repository에 접근하여 배치의 정보를 얻는다.

7.17 - JobOperator

JobOperator는 Job을 제어하는 모니터링 작업을 위해 사용된다. JobOperator는 JobRegistry, JobExplorer, JobLauncher, JobRepository 클래스의 설정이 필수적이며, Job의 InstanceId, ExecutionId, JobName을 이용하여 Job을 제어한다.

7.18 - Skip/Retry/Repeat

Skip, Retry, Repeat은 효율적인 배치수행을 위해 필요한 기능들이다. Repeat 정책에 따라 Step과 Chunk가 반복적으로 수행되면서 데이터 Read, Process, Write 과정이 일어나는데, 여기서 Skip과 Retry 이용해 배치작업을 효율적으로 수행할 수 있다. 각 기능이 쓰이는 위치는 다음 그림을 참고한다.

Skip/Retry/Repeat

개요

Skip, Retry, Repeat은 효율적인 배치수행을 위해 필요한 기능들이다. Repeat 정책에 따라 Step과 Chunk가 반복적으로 수행되면서 데이터 Read, Process, Write 과정이 일어나는데, 여기서 Skip과 Retry 이용해 배치작업을 효율적으로 수행할 수 있다. 각 기능이 쓰이는 위치는 다음 그림을 참고한다.

image

설명

Skip

Skip은 데이터를 처리하는 동안 설정된 Exception이 발생했을 경우, 해당 데이터 처리를 건너뛰는 기능이다. 데이터의 사소한 오류에 대해 Step의 실패처리 대신 Skip을 함으로써, 배치수행의 빈번한 실패를 줄일 수 있게 한다.

Skip 로직 구성(Configuring Skip Logic)

Skip설정은 Job설정파일의 <chunk> 내부에서 이루어진다. 일반 chunk 설정(reader, processor, writer, commit-interval)에 추가로 skip-limit 을 지정한다. 또한 <skippable-exception-classes>를 지정할 수 있으며 (이것은 옵션?) 상세설명은 다음 표를 참고한다.

항목설명
skip-limitSkip 할 수 있는 최대 횟수를 지정
default=0 이므로 꼭 지정해줘야 Skip기능 사용 할 수 있음 (확인필요)
<skippable-exception-classes><include>skip 해야하는 Exception 범위를 지정
<skippable-exception-classes><exclude>include로 지정한 exception의 하위 exception 중, Skip하지 않을 Exception 지정

✔ <skippable-exception-classes> 항목의 Exception 범위 지정은 데이터 성격에 대해 잘 알고 있는 사람이 결정해야 한다.
예를 들어 공급업체의 데이터 처리는 Skip 하도록 설정할 수 있지만 금융거래에서 데이터 처리는 Skip이 되어선 안되기 때문이다.

다음 예시는 FlatFileItemReader로 데이터를 읽는 과정에서 <include> 로 설정된 FlatFileParseException 발생 시 Skip이 일어나도록 설정이 되어 있고, 이렇게 발생한 Skip은 10번까지만 허용한다. 그 이상의 Skip이 발생한다면 Step을 실패처리한다.

<step id="step1">
 <tasklet>
  <chunk reader="flatFileItemReader" writer="itemWriter" commit-interval="10" skip-limit="10">
   <skippable-exception-classes>
    <include class="org.springframework.batch.item.file.FlatFileParseException"/>
   </skippable-exception-classes>
  </chunk>
 </tasklet>
</step>

✔ 내부적으로 Skip의 횟수를 관리하는 Counter가 있는데 read, process, write 별로 분리되어 있으며, skip-limit에는 각 Counter의 합계가 적용된다. 데이터 성격에 따라 Skip관리를 위해 Counter를 유용하게 사용할 수 있다.

위 예시에서 한가지 문제가 있는데 FlatFileItemReader을 제외한 Exception이 발생하는 경우 Job을 실패로 처리하는 것이다. 이러한 처리가 옳을 수도 있지만, 다음예시 처럼 <exclude>를 설정하여 지정한 Exception클래스와 그 하위에러가 발생할 경우에 Skip하지 않고, 에러를 발생시키도록 표현하는 것이 명확하다.

<step id="step1">
 <tasklet>
  <chunk reader="flatFileItemReader" writer="itemWriter" commit-interval="10" skip-limit="10">
   <skippable-exception-classes>
    <include class="java.lang.Exception"/>
    <exclude class="java.io.FileNotFoundException"/>
   </skippable-exception-classes>
  </chunk>
 </tasklet>
</step>

관련예제

건너뛰기(Skip) 기능 예제

Retry

Retry는 데이터를 Processing, Writing 하는 동안 설정된 Exception이 발생했을 경우, 지정한 정책에 따라 데이터 처리를 재시도하는 기능이다. Skip 과 마찬가지로 Retry를 함으로써, 배치수행의 빈번한 실패를 줄일 수 있게 한다.

Retry 로직 구성 (Configuring Retry Logic)

Retry설정은 Job설정파일의 <chunk> 내부에서 이루어진다. 일반 chunk 설정(reader, processor, writer, commit-interval)에 추가로 retry-limit 을 지정한다. 또한 <retryable-exception-classes>를 지정할 수 있으며 상세설명은 하단 표를 참고한다.

항목설명
retry-limitRetry 할 수 있는 최대 횟수를 지정
<retryable-exception-classes><include>Retry 해야하는 Exception 범위를 지정
<retryable-exception-classes><exclude>include로 지정한 exception의 하위 exception 중, Retry하지 않을 Exception 지정

✔ Item Processing과 Item Writing 과정에서만 Retry 된다.

데이터를 처리하는 Read과정에서 주로 발생하는 FlatFileParseException 대한 문제는 대부분 Skip에서 처리가 된다.
반면에, Process 과정과 Write과정에서 발생하는 데이터 선점에 대한 DeadlockLoserDataAccessException 등은 Retry를 통해 해결할 수 있다. 즉, 다른 프로세스에서 처리중인 데이터에 새로운 프로세스가 접근하는 경우 Lock이 걸려 있어 에러가 발생하는데 잠시 후 재시도 하면 성공할 수 있는 것이다.

✔ Read과정까지 성공한 데이터는 캐쉬에 저장된다. 그러므로 재시도가 일어날 경우 캐쉬의 데이터를 가져와 Process 과정부터 다시 수행한다.

✔ retryable exception은 기본적으로 rollback을 유발하므로 너무 많은 Retry는 성능을 저하시킬 수 있으므로 주의해야 한다.

다음 예시는 데이터를 처리하는 과정에서 <include> 로 설정된 DeadlockLoserDataAccessException 발생 시 Retry가 일어나도록 설정이 되어 있고, 이렇게 발생한 Retry는 3번까지만 허용한다. 그 이상의 Retry가 발생한다면 Step을 실패처리한다. 최초 데이터를 읽는 것부터 한번의 시도로 취급하므로, 예제에서는 두번의 시도를 더 할 수 있다.

<step id="step1">
 <tasklet>
  <chunk reader="itemReader" writer="itemWriter" commit-interval="2" retry-limit="3">
   <retryable-exception-classes>
    <include class="org.springframework.dao.DeadlockLoserDataAccessException"/>
   </retryable-exception-classes>
  </chunk>
 </tasklet>
</step>

기타 설정

  • retry-policy : 에서 retry-limit등을 직접 입력하지 않고, 사용자가 상세하게 작성한 Retry 정책을 적용
<job id="retryPolicyJob" xmlns="http://www.springframework.org/schema/batch">
 <step id="retryPolicyStep">
  <tasklet>
    <chunk reader="reader" writer="writer" commit-interval="100" retry-policy="retryPolicy" />
  </tasklet>
 </step>
</job>
 
<bean id="retryPolicy" class="org.springframework.batch.retrypolicy.SimpleRetryPolicy">
 <property name="maxAttempts" value="3" /> 							
</bean>
  • backOffPolicy : 다시 Retry를 시도하기까지의 지연시간 (단위:ms), 처리시간이 긴 데이터가 있을 경우 backOffPolicy로 재시도의 시간간격을 조절하여 Retry 설정 변경 가능
<job id="job1" job-repository="jobRepository"> 
 <step id="step1" parent="stepParent"> 
     ... 
 </step> 
</job>
 
<bean id="stepParent" class="org.springframework.batch.core.step.item.FaultTolerantStepFactoryBean" abstract="true"> 
 <property name="backOffPolicy"> 
  <bean class="org.springframework.batch.retry.backoff.FixedBackOffPolicy" 
   <property name="backOffPolicy" value="2000" /> 
  </bean> 
 </property> 
</bean>

Retry 템플릿(RetryTemplate)

조금 더 견고하게 실패를 처리하고, 바로 이어서 시도해서 데이터 처리를 성공할 수 있다고 생각되는 경우, 자동으로 실패한 연산을 재시도하는 것이 도움이 된다. 예를 들어, 네트웍 문제로 실패한 웹 서비스나 ROM 서비스나 데이터베이스 갱신에서 발생한 DeadLockLoserException을 예로 들 수 있다. 스프링 배치에서는 이러한 연산을 자동으로 재시도 하기 위한 RestryOperations 전략을 갖고 있다.

public interface RetryOperations {
 
    <T> T execute(RetryCallback<T> retryCallback) throws Exception;
 
    <T> T execute(RetryCallback<T> retryCallback, RecoveryCallback<T> recoveryCallback) 
        throws Exception;
 
    <T> T execute(RetryCallback<T> retryCallback, RetryState retryState) 
        throws Exception, ExhaustedRetryException;
 
    <T> T execute(RetryCallback<T> retryCallback, RecoveryCallback<T> recoveryCallback, 
        RetryState retryState) throws Exception;
 
}

콜백은 재시도하는 비즈니스 로직을 넣을 수 있는 간단한 인터페이스다.

public interface RetryCallback<T> {
 
    T doWithRetry(RetryContext context) throws Throwable;
 
}

콜백이 실행되고 예외가 발생해서 실패하는 경우, 성공할때까지 재시도 하게 된다. 또는 구현 여부에 따라 취소 여부를 결정한다.
가장 간단한 일반적으로 목적의 RetryOperations의 구현은 RetryTemplate이다.

RetryTemplate template = new RetryTemplate();
 
template.setRetryPolicy(new TimeoutRetryPolicy(30000L));
 
Foo result = template.execute(new RetryCallback<Foo>() {
 
    public Foo doWithRetry(RetryContext context) {
        // Do stuff that might fail, e.g. webservice operation
        return result;
    }
 
});

위 예제에서는 웹 서비스 호출을 실행하고 결과를 사용자에게 반환한다. 만약 호출이 실패하면 타임아웃이 될 때까지 재시도한다.

RetryContext

RetryCallback의 파라메터로 RetryContext가 있다. 콜백에서는 이 context를 무시하지만, 만약 필요하다면 반복되는 동안에 데이터를 저장하는 속성 가방(Attribute Bag)으로 사용할 수 있다.

RecoveryCallback

Retry가 모두 사용되면, RetryOperation 은 다른 콜백(RecoveryCallback)에게 콜백 제어권을 넘길 수 있다. 이런 기능을 사용하려면 같은 방법으로 콜백에 전달하면 된다.

Foo foo = template.execute(new RetryCallback<Foo>() {
    public Foo doWithRetry(RetryContext context) {
        // business logic here
    },
  new RecoveryCallback<Foo>() {
    Foo recover(RetryContext context) throws Exception {
          // recover logic here
    }
});

비즈니스로직이 템플릿 중단을 결정하기 전에 성공되지 못한다면, 클라이언트는 RecoveryCall을 통해 다른 처리를 할 기회가 주어진다.

Stateless Retry

단순한 Retry 방법은 RetryTemplate이 성공이나 실패를 할때까지 계속 시도하는 루프이다. RetryContext는 재시도 할건지 취소할 건지를 결정하는데 사용하는 상태를 포함하지만, 이 상태는 스택에 저장되고, 어디서나 접근하도록 글로벌하게 저장할 필요는 없다. 그러므로 우리는 이 방법을 무상태 재시도(Stateless Retry)라고 부른다. 무상태 재시도와 상태 유지 재시도 사이의 차이는RetryPolicy의 구현여부이다. (RetryTemplate은 둘 다 제어할 수 있다.) 무상태 재시도에서 Retry가 실패될 때, 콜백은 항상 동일한 쓰레드에서 수행된다.

Stateful Retry

트랜잭션 처리하는 자원을 무효화 시키는 실패는 몇가지 고려사항이 있다. (일반적으로)트랜잭션 처리가 없기 때문에 간단한 원격호출에 적용될 뿐만아니라 하이버네이트를 사용처럼 데이터베이스 갱신에도 적용된다. 이럴 경우 트랜잭션이 롤백되서, 다시 유효한 트랜잭션으로 시작할 수 있도록 실패가 되자마자 예외를 다시 던지는게 상황에 맞다.

예외를 다시 던지고(re-throw) 롤백하는 것은 남겨진 RetryOperations.execute메소드와 잠재적으로 스택에 있는 context를 손실하게 되기 때문에, 이런경우 무상태 재시도는 좋은 방법이 아니다. 이 손실을 피하기 위해서는 스택에서 context를 빼내서, 힙 저장 영역에 넣어두는 저장 전략을 도입해야 한다. 이러한 목적으로 스프링 배치는 RetryContextCache를 제공한다. RetryContextCache의 기본 구현은 단순하게 Map을 사용해서 메모리에 저장한다. (비록 클러스터 환경에서는 지나칠지라도) 클러스터 환경에서 다수의 프로세스를 처리하는 고급 사용법은 여러 종류의 클러스터 캐시를 사용하는 RetryContextCache 구현을 고려하자.

RetryOperations의 책임의 일부는 새로운 실행 (그리고 일반적으로 새 트랜잭션에 싸여)에 돌아 왔을 때 실패한 작업을 인식하는 것이다. 이 원할히 하기 위해, 스프링 배치는 RetryState 추상화를 제공한다. 실패한 작업을 인식하는 방법은 재시도의 여러 호출에 대한 상태를 식별하는 것이다. 상태를 확인하기 위해, 사용자는 Item을 식별하는 고유키를 리턴하는 RetryState객체를 제공해야 한다. 식별자는 RetryContextCache에서 키로 사용된다.

✔ 주의

RertyState에 이해 리턴되는 키에서 Object.equals()와 Object.hashCode() 구현은 매우 조심해야 한다. 가장 추천하고 싶은 것은 Item을 구분할 수 있는 비즈니스키를 사용하는 것이다. JMS 메시지 경우에 messageID 가 사용될 수 있다.

Retry 정책(Retry Policies)

RetryTemplate에서 execute 메소드의 재시도나 실패를 결정하는건 RetryPolicy에 의해서 결정된다. RetryPolicy는 RetryContext의 팩토리가 되기도 한다. RetryTemplate은 RetryContext를 만들기 위해서 현재 정책을 사용해야 할 책임을 갖으며, 시도마다 RetryCallback에 이를 전달한다. 콜백이 실패한 후에 RetryTemplate은 상태를 갱신하려고 RetryPolicy를 호출하며(RetryContext에 저장된다), 그 다음으로 또 다른 시도를 할 수 있는 경우에 RetryPolicy를 호출해서 정책을 문의하게 된다. (예를 들어, 제한에 걸렸거나 타입아웃이 되버린 것처럼) 또 다른 시도를 하지 못하게 되면, 정책은 다 사용된 상태를 관리하는 책임을 진다. 단순히 구현하자면 RetryExhastedException을 던지게 되고, 관련된 트랜잭션은 롤백된다. 좀 더 정교하게 구현하자면,트랜잭션을 손상하지 않고 유지할 수 있는 경우에 복구 행동을 시도할 수 있다.

✔ Tip

실패는 태생적으로 재시도 여부가 결정된다. 일부 예외에서는 언제나 비지니스 로직 문제로 던져지므로 재시작 하는데 전혀 도움이 되지 못한다. 그러므로 모든 예외 타입에 대해서 재시도를 하면 안된다. 오로지 재시도를 할 수 있는 예외에만 집중해야 한다. 일반벅으로 더 공격적으로 재시도를 처리하는 것은 비즈니스 로직에 해가 되지는 않지만, 미리 실패를 알고 있는 것을 재시도하면서 시간을 소비하는 경우라면 비경제적이다.

스프링 배치는 범용목적의 statelses RetryPolicy의 구현을 제공한다. 예를 들어 SimpleRetryPolicy,TimeoutRetryPolicy 가 아래의 예에서 사용된다. SimpleRetryPolicy는 정해진 최대횟수만큼 예외유형의 리스트에 있는 재시도를 허락한다. SimpleRetryPolicy는 재시도되면 안되는 “치명적”예외 목록을 가지고 있으며, 재시도 동작 그 이상의 미세한 제어를 사용할 수 있도록 재시도가능한 목록에 덮어쓰기된다.

SimpleRetryPolicy policy = new SimpleRetryPolicy(5);
// Retry on all exceptions (this is the default)
policy.setRetryableExceptions(new Class[] {Exception.class});
// ... but never retry IllegalStateException
policy.setFatalExceptions(new Class[] {IllegalStateException.class});
// Use the policy...
RetryTemplate template = new RetryTemplate();
template.setRetryPolicy(policy);
template.execute(new RetryCallback<Foo>() {
   public Foo doWithRetry(RetryContext context) {
      // business logic here
}
});

사용자는 더 많은 재정의된 결정에 자신의 재시도 정책을 구현해야 할 수도 있다.

Backoff 정책(Backoff Policies)

일시적인 실패 후에 재시도되 때, 실패의 원인이 되는 일부 문제들은 단지 잠시 기다리기만 해도 해결되는 경우가 있기 때문에, 많은 경우 다시 시도하기 전에 잠시 기다리는게 도움이 되기도 한다. RetryCallback이 실패한 경우,

public interface BackoffPolicy {
 
    BackOffContext start(RetryContext context);
 
    void backOff(BackOffContext backOffContext) 
        throws BackOffInterruptedException;
 
}

BackoffPolicy 는 자유롭게 방법을 선택해 구현하면 된다. 스프링 배치에서 제공되는 정책은 특별히 Object.waite()를 사용한다. 공통적인 사용은 두 재시도가 락에 걸리고 둘다 실패하는 것을 피하기 위해 긴 기다림이 증가하는 BackOff가 있다. 이러한 목적으로 스프링배치에서 ExponentialBackoffPolicy를 제공한다.

리스너(Listeners)

종종 서로 다른 다수의 반복에서 공통으로 걸리는하는 추가적인 콜백을 받아오는 것이 유용할 때가 있다. 이러한 목적으로 스프링 배치는 RetryListener 인터페이스를 제공한다. RetryTemplate은 사용자가 RetryListener를 등록하도록 해주며, 반복 동안에 이용할 수 있는 RetryContext와 Throwable 와 함께 콜백에 전해진다.

public interface RetryListener {
 
    void open(RetryContext context, RetryCallback<T> callback);
 
    void onError(RetryContext context, RetryCallback<T> callback, Throwable e);
 
    void close(RetryContext context, RetryCallback<T> callback, Throwable e);
}

open과 close콜백이 모든 재시도 전후에 호출되며, onError()는 개별적인 RetryCallback 호출에 적용된다. 또한 close메소드는 RetryCallback에 의해서 마지막에 던저진 에러가 있는 경우에 Throwable을 받아올 수 있다. 다수의 리스너를 리스트로 갖으며 이는 순서가 있다. open메소드의 경우 동일한 순서로 호출되며,onError()와 close()는 역순으로 호출된다.

선언적 Retry(Declarative Retry)

때때로 발생할 때마다 재시작하고 싶은 비즈니스 처리과정이 있다. 고전적인 예로 원격 서비스 호출이 있다. 스프링 배치는 이러한 목적에 딱 맞는 RetryOperations에서 호출되는 메소드를 감싸는 AOP 인터셉터를 제공한다.RetryOperationsInterceptor는 가로챈 메소드를 실행하고, 제공된 RetryTemplate에 있는 RetryPolicy에 따라서 실패를 재시도 한다.
아래는 remoteCall을 호출하는 메소드 서비스 호출을 재시도 하는데 스프링 AOP 네임스페이스를 사용하는 선언적인 재시도의 예제다.

<aop:config>
    <aop:pointcut id="transactional" expression="execution(* com..*Service.remoteCall(..))" />
    <aop:advisor pointcut-ref="transactional" advice-ref="retryAdvice" order="-1"/>
</aop:config>
 
<bean id="retryAdvice" class="org.springframework.batch.retry.interceptor.RetryOperationsInterceptor"/>

이 예에서는 인터셉터 내에 있는 기본 RetryTemplate을 사용한다. 리스너나 정책을 변경하기 위해 인터셉터에 RetryTemplate을 적용하는 것이 필요하다.

관련예제

재시도(Retry) 예제

Repeat

배치는 작업의 구성요소인 Step과 그 하위의 Chunk의 지속적인 반복수행으로 이루어진다. 여기서 반복수행은 Repeat정책을 따르며 구성요소별로 반복을 발생시킴으로써 배치를 수행하는 기능이다.

Repeat 템플릿(RepeatTemplate)

배치 처리 과정은 단순하게 최적화되거나 Job의 일부요소의 반복적인 행동이다. 스프링 배치는 반복을 전략적으로 일반화하고, iterator 프레임워크를 제공하기 위해 RepeatOperations 인터페이스를 가지고 있다.
RepeatOperations 인터페이스는 다음과 같다.

 public interface RepeatOperations {
    RepeatStatus iterate(RepeatCallback callback) throws RepeatException;
}

콜백은 반복되는 비즈니스 로직을 추가하도록 해주는 간단한 인터페이스다.

public interface RepeatCallback {
    RepeatStatus doInIteration(RepeatContext context) throws Exception;
}

콜백은 반복이 끝났다고 결정될 때까지 반복적으로 실행된다. 이 인터페이스에서 반환하는 값은 RepeatStatus.CONTINUABLE 또는 RepeatStatus.FINISHED 이다. RepeatStatus는 더 수행할 작업이 있는지에 대한 repeat 수행 호출정보를 전달한다. 일반적으로 RepeatOperations의 구현은 RepeatStatus를 확인하고, 이것을 이용해 수행을 종료할지 반복할지에 대한 결정을 내린다. 호출자에게 더 이상 할 일이 없다는 신호를 보내고자 하는 모든 콜백은 ExistStatus.FINISHED를 반환하면 된다.
RepeatOperations의 가장 간단하고 일반적인 구현은 RepeatTemplate이다. 다음처럼 사용한다.

RepeatTemplate template = new RepeatTemplate(); 
template.setCompletionPolicy(new FixedChunkSizeCompletionPolicy(2)); 
template.iterate(new RepeatCallback() {
		public ExitStatus doInIteration(RepeatContext context) {
		// Do stuff in batch...
		return ExitStatus.CONTINUABLE;
		} 
});

이 예에서는 계속 할 일이 있다는 것을 보여주는 ExitStatus.CONTINUABLE을 반환한다. 더 이상 수행할 것이 없다는 요청을 보내고 싶다면 ExitStatus.FINISHED 을 반환한다.

  • RepeatContext

RepeatContext는 RepeatCallback의 메소드 인자다. 많은 콜백들은 단순하게 context를 무시하지만, 반복 하는 동안에 일시적으로 사용할 필요가 있는 데이터를 저장하는 속성 가방 (Attribute Bag)으로서 사용될 수 있다. iterate 메소드가 결과 를 반환한 후에, context는 더 이상 존재하지 않게 된다. RepeatContext는 처리 과정에서 내제된 반복이 필요한 경우 부모 context를 갖게 된다. 종종 부모 context는 반복되는 호출 사이에 공유할 필요가 있는 데이터를 저장하는데 유용하다.

  • RepeatStatus

ExitStatus는 스프링 배치에서 처리 과정이 끝났고, 처리가 성공인지 아닌지를 지정하는 목적으로 사용한다. 또는 배치나 반복의 종료 상태에 대한 정보(textual information)를 전달하는데 사용된다. 이 정보는 종료 코드의 형태와 자유로운 형식의 문자상태에 대한 설명이 된다.

프로퍼티 이름설명
CONTINUABLE작업이 남아 있음
FINISHED더 이상의 반복 없음

RepeatStatus 값은 and 메소드를 사용해 논리 AND수행과 결합될 수 있다. 즉, 어떤 수행의 상태가 FINISHED 면 결과는 FINISHED 다.

완료 정책(Completion Policies)

RepeatTemplate 내에서 iterate 메소드에 있는 루프의 종료는 CompletionPolicy에 의해서 결정된다. CompletionPolicy 는 RepeatContext에 대한 팩토리도 된다. RepeatTemplate은 RepeatContext를 생성하는 정책을 이용해 반복 중 모든 단계에서 RepeatCallback에게 전달해야 하는 책임을 가지고 있다. 콜백이 완료된 후에 RepeatTemplate의 doInIteration는 상태를 갱신해야 하는지(RepeatContext에 저장될 것인지) 여부를 CompletionPolicy에게 물어보게 된다. 그 다음으로 반복이 완료된 경우에 정책을 요청하게 된다.

스프링 배치는 일반적인 목적으로 사용되는 간단한 CompletionPolicy 구현체를 제공한다. 위 예에서 사용한 SimpleCompletionPolicy을 예로 들 수 있다. SimpleCompletionPolicy는 고정된 시간만큼만 실행을 허용한다. (ExistStatus.FINISHED로 정해진 시간보다 강제적으로 일찍 완료할 수 있다.)

예외 핸들링(Exception Handling)

RepeatCallback 내에서 예외가 던져지는 경우, RepeatTemplate은 예외를 다시 던져야 하는지를 결정하는데 ExceptionHandler에게 의견을 묻게 된다.

public interface ExceptionHandler {
	void handleException(RepeatContext context, Throwable throwable)
		throws RuntimeException;
}

일반적인 사용방법은 주어진 타입의 예외발생 횟수를 세고, 한도에 도달했을때 실패한다. 이러한 목적에 맞게 스프링 배치는 SimpleLimitExceptionHandler와 조금 더 유연한 RethrowOnThresholdExceptionHandler를 제공한다. SimpleLimitExceptionHandler는 limit 프로퍼티와 현재 예외를 비교하는 예외 타입을 가지고 있다. 이 때 제공된 타입의 모든 하위 클래스들도 처리에 포함시킨다. 주어진 타입의 예외는 한계에 도달할 때까지는 무시되었다가 다시 던져지게 된다. 이러한 다른 예외 타입들도 항상 다시 던진다.
SimpleLimitExceptionHandler의 선택 가능한 중요한 프로퍼티는 useParent boolean 표시다. 기본값은 false기 때문에, 한계는 현재 RepeatContext에서만 설명된다. true로 설정되었을 때 한계는 내제된 반복(nested iteration)에서 형제context에 걸쳐 유지된다.

리스너(Listeners)

종종 서로 다른 다수의 반복에서 공통으로 걸리는 추가 콜백을 받아오는 것이 유용한 경우가 있다. 이러한 목적으로 스프링 배치는 RpeatListener 인터페이스를 제공한다. RepeatTemplate은 사용자가 RepeatListener를 등록할 수 있게 해준다. 그리고 콜백 반복 중에 이용할 수 있도록 RepeatContext와 RepeatStatus를 전달한다.

public interface RepeatListener {
	void before(RepeatContext context);
	void after(RepeatContext context, RepeatStatus result);
	void open(RepeatContext context);
	void onError(RepeatContext context, Throwable e);
	void close(RepeatContext context);
}

open과 close 콜백은 개별적언 RepeatCallback 호출에 적용되어 before, after, onError 전후에 호출된다. 또한 다수의 리스너를 리스트로 갖으며 이는 순서가 있다. open과 before는 동일한 순서로 호출되며 after(), onError(), close()는 역순으로 호출된다.

병렬처리(Parallel Processing)

RepeatOperations의 구현은 순차적으로 콜백을 실행하도록 제한하지 못한다. 구현은 동시에 콜백이 실행할 수 있도록 하는 건 제일 중요하다. 이 때문에 스프링 배치는 RepeatCallback을 실행하는데 TaskExecutor 전략을 사용하는 TaskExecutorRepeatTemplate을 제공한다. 기본적으로 (일반 RepeatTemplate과 같은) 동일한 쓰레드에 있는 전체반복을 수행하는 SynchronousTaskExecutor를 사용한다.

선언적 반복(Declarative Iteration)

때때로 발생할 때마다 반복하고 싶은 비즈니스 처리과정이 있다. 고전적인 예제로 메세지 파이프라인의 최적화가 있다.메세지를 자주 받게 되는 경우, 매세지 마다 개별적인 트랜잭션으로 처리하는 비용을 참기 보다는 메세지를 배치로 처리 하는게 좀더 효율적이다. 스프링 배치는 이 목적에 맞게 RepeatOperations에서 메소드 호출을 감싸는 AOP 인터셉터를 제공한다. RepeatOperationsInterceptor는 가로챈 메소드를 실행해여 제공된 RepeatTemplate의 CompetionPolicy에 따라서 반복하게 된다.
여기서는 스프링 AOP 네임스페이스를 사용해서 호출되는 processMessage 메소드를 호출하는 서비스를 반복하는데 선언적인 반복의 예를 보자.

 <aop:config>
    <aop:pointcut id="transactional"
        expression="execution(* com..*Service.processMessage(..))" />
    <aop:advisor pointcut-ref="transactional"
        advice-ref="retryAdvice" order="-1"/>
</aop:config>
 
<bean id="retryAdvice" class="org.spr...RepeatOperationsInterceptor"/>

이 예에서는 인터셉터 내에 있는 기본 RetryTemplate을 사용한다. 리스너나 정책을 변경하기 위해 인터셉터에 RetryTemplate을 적용하는 것이 필요하다.
가로챈 메소드가 void 반환 타입이라면, 인터셉터는 언제나 ExistStatus.CONTINUABLE을 반환한다. (그렇기 때문에 CompletionPolicy가 한정된 종료 지점이 없는 경우라면 무한 반복의 위험이 있다.) 만약 그렇지 않다면 가로챈 메소드에서 ExitStatus.FINISHED를 반환하는 지점이 되는 null을 반환할 때까지 ExitStatus.CONTINUABLE을 반환한다. 그래서 대상 메소드 내에 있는 비즈니스 로직은 null을 반환하거나 RepeatTemplate에서 제공하는 ExceptionHandler에 의해서 다시 던진 예외를 던져서 더 할 일이 없다는 신호를 보낼 수 있다.

참고자료

Skip : http://static.springsource.org/spring-batch/reference/html/configureStep.html#configuringSkip
Retry : http://static.springsource.org/spring-batch/reference/html/configureStep.html#retryLogic http://static.springsource.org/spring-batch/reference/html/retry.html
Repeat : http://static.springsource.org/spring-batch/reference/html/repeat.html

7.19 - MultiDataProcessing

배치 수행 시 여러 리소스를 처리해야 할 경우, 전자정부 배치 프레임워크는 MultiData Processing을 통해 다수의 리소스를 읽고 다수(N→N) 또는 하나(N→1)의 결과로 처리하는 기능을 제공한다. MultiResourceItem은 여러 리소스를 읽어 각각의 결과를 생성하고, CompositeItem은 여러 리소스를 하나의 결과로 처리한다.

MultiDataProcessing

개요

배치 수행시 다수의 리소스를 처리하고자 할 경우에는 일반적인 Job설정으로 처리할 수 없다. 전자정부 배치프레임워크에서는 MultiData Processing을 통해 다수의 리소스를 읽어 다수의 결과로 처리하거나 다수의 리소스를 읽어 하나의 결과로 처리하는 기능을 제공한다.

설명

다수(N개)의 리소스를 처리하는 방식은 N→1, N→N으로 구분된다.

  • MultiResourceItem처리: N개의 대상을 읽은 후, 읽은 개수만큼의 결과물을 만들어낸다.
  • CompositeItem처리: N개의 대상을 읽은 후, 하나의 결과물을 만들어낸다.

두 방식을 개념적으로 비교하면 아래와 같다.

image

MultiResourceItem 처리

다수의 파일을 대상으로 동일한 유형의 Batch처리를 하고자 할 경우 MultiResourceItemReader를 사용하면 편리하다.
예를 들어, 아래와 같이 ‘file~‘로 시작하는 파일명을 가진 파일들에 대해 일괄 변경을 수행하고자 할 경우에도 적용 가능하다.

file-1.txt  file-2.txt  ignored.txt

Job 설정

Job수행에 사용되는 Reader및 Writer설정은 일반적인 Job과 동일하다.

<job id="multiResourceIoJob" xmlns="http://www.springframework.org/schema/batch">
    <step id="multiResourceIoStep1">		
        <tasklet>
	    <chunk reader="itemReader" processor="itemProcessor" writer="itemWriter" commit-interval="2"/>
	</tasklet>	
    </step>
</job>

Reader 설정

MultiResourceItemReader를 통해 여러 개의 리소스를 읽어온 다음, 1개의 리소스를 처리하는 Reader에게 데이터처리를 위임한다.
이 때, input resource경로에 *를 사용하여 다수의 파일을 처리 가능하다.

<bean id="itemReader" class="org.springframework.batch.item.file.MultiResourceItemReader" scope="step">
    <property name="delegate">
        <bean class="org.springframework.batch.item.file.FlatFileItemReader">
            <property name="lineMapper">
	        <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
		    <property name="lineTokenizer">
		        <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
			    <property name="delimiter" value="," />
			    <property name="names" value="name,credit" />
			</bean>
		    </property>
		    <property name="fieldSetMapper">
			<bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
			    <property name="targetType" 
                               value="egovframework.brte.sample.common.domain.trade.CustomerCredit" />
			</bean>
         	    </property>
		 </bean>
	     </property>
	 </bean>
    </property>
    <property name="resources" value="classpath:data/input/file-*.txt" />
</bean>

Writer 설정

MultiResourceItemWriter를 통해 출력파일의 개수를 지정한 다음, 1개의 리소스를 처리하는 Writer에게 데이터처리를 위임한다.

<bean id="itemWriter" class="org.springframework.batch.item.file.MultiResourceItemWriter" scope="step">
    <property name="resource" value="#{jobParameters['output.file.path']}" />
    <property name="itemCountLimitPerResource" value="6" />
    <property name="delegate" ref="delegateWriter" />
</bean>
 
<bean id="delegateWriter" class="org.springframework.batch.item.file.FlatFileItemWriter">
    <property name="lineAggregator">
        <bean class="org.springframework.batch.item.file.transform.DelimitedLineAggregator">
            <property name="delimiter" value="," />
            <property name="fieldExtractor">
                <bean class="org.springframework.batch.item.file.transform.BeanWrapperFieldExtractor">
                    <property name="names" value="name,credit" />
		 </bean>
            </property>
        </bean>
    </property>
</bean>

CompositeItem처리

스프링 배치에서는 Composite처리와 관련하여 CompositeWriter만을 제공하고 있다. 이에 전자정부 배치프레임워크에서는 CompositeReader를 추가적으로 제공한다.

처리 프로세스

CompositeReader의 일반적인 처리 프로세스는 아래와 같다.

image

주의! CompositeReader는 등록된 모든 Reader로부터 데이터를 한 라인씩 순서대로 읽어와서 배열에 넣어주는 역할까지 수행한다.따라서,Writer를 바로 사용하면 안되고 Processor에서 배열을 읽어서 처리하는 과정이 반드시 필요하다.

처리 유형

CompositeReader에는 Reader에서 리소스를 처리해서 Processor로 VO를 전달하는 유형과 Reader자체를 그대로 Processor로 전달하는 유형으로 나뉘며,
Processor에서는 전달된 데이터 타입에 맞게 비즈니스 로직을 구현할 수 있다.

유형 I - VO(ValueObject)를 Processor에 전달하는 유형유형 II - Reader를 Processor에 전달하는 유형
imageimage
<bean id="compositeItemReader"
   class="egovframework.brte.core.item.composite.reader.EgovCompositeFileReader">
    <property name="itemsMapper">
        <bean
           class="egovframework.brte.core.item.composite.EgovCompositeItemMapper" />
    </property>
    <property name="returnType" value="vo" />
    <property name="itemReaderList">
        <list>
            <ref bean="itemReader1" />
            <ref bean="itemReader2" />
        </list>
    </property>
</bean>
``` | 

```xml
<bean id="compositeItemReader"
   class="egovframework.brte.core.item.composite.reader.EgovCompositeFileReader">
    <property name="itemsMapper">
        <bean
           class="egovframework.brte.core.item.composite.EgovCompositeItemMapper" />
    </property>
    <property name="returnType" value="reader" />
    <property name="itemReaderList">
        <list>
            <ref bean="itemReader1" />
            <ref bean="itemReader2" />
        </list>
    </property>
</bean>
``` |

#### Job 설정

 CompositeItem처리를 위한 Job설정은 reader에 compositeItemReader를 설정하고, processor를 지정해야 한다.  

```xml
<job id="compositeItemJob" xmlns="http://www.springframework.org/schema/batch">
    <step id="compositeItemStep">
        <tasklet>
	    <chunk reader="compositeItemReader" processor="itemProcessor" writer="itemWriter"
	       commit-interval="5" />
        </tasklet>
    </step>
</job>

Reader 설정

ComposteItemReader를 설정하기 위해서는 Reader로 사용할 Class, returnType, itemReaderList 항목을 작성해야 한다.

<bean id="compositeItemReader"
   class="egovframework.brte.core.item.composite.reader.EgovCompositeFileReader">
    <property name="itemsMapper">
        <bean
           class="egovframework.brte.core.item.composite.EgovCompositeItemMapper" />
    </property>
    <property name="returnType" value="vo" />
    <property name="itemReaderList">
        <list>
            <ref bean="itemReader1" />
            <ref bean="itemReader2" />
        </list>
    </property>
</bean> 
 
<bean id="itemReader1" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step">
    <property name="resource" value="#{jobParameters[inputFile]}" />
    <property name="lineMapper">
        <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
            <property name="lineTokenizer">
                <bean class="org.springframework.batch.item.file.transform.FixedLengthTokenizer">
                    <property name="names" value="name,credit" />
                    <property name="columns" value="1-9,10-11" />
                </bean>
            </property>
            <property name="fieldSetMapper">
                <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
                    <property name="targetType" value="egovframework.brte.sample.common.domain.trade.CustomerCredit" />
                </bean>
            </property>
        </bean>
    </property>
</bean>	
 
<bean id="itemReader2" class="org.springframework.batch.item.file.FlatFileItemReader" scope="step">
    <property name="lineMapper">
        <bean class="org.springframework.batch.item.file.mapping.DefaultLineMapper">
	    <property name="lineTokenizer">
                <bean class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer">
                    <property name="delimiter" value=","/>
                    <property name="names" value="name,credit" />
                </bean>
            </property>
            <property name="fieldSetMapper">
                <bean class="org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper">
                    <property name="targetType" value="egovframework.brte.sample.common.domain.trade.CustomerCredit" />	
                </bean>
            </property>
        </bean>
    </property>
    <property name="resource" value="#{jobParameters[inputFile]}" />
</bean>

✔ CompositeItemReader에서 사용가능한 Class는 총 3개가 존재하며 각 Class별 특징은 다음과 같다.

종류설명
EgovCompositeFileReaderFlatFile(FixedLength,Delimited), XMLFile 등 파일처리를 위한 용도로 사용
EgovCompositeCursorReaderJdbcCursorItemReader를 통해 DB리소스 처리를 하고자 할 경우에 사용
EgovCompositePagingReaderJdbcPagingItemReader, IbatisPagingItemReader 등 Paging단위로 DB리소스 처리를 하고자 할 경우에 사용

Processor 설정

CompositeItemReader를 통해 넘어온 VO 또는 Reader를 Processor에서 꺼내와서 처리하는 로직을 구현해야 한다.

public class FileItemProcessor implements ItemProcessor<EgovCompositeDataProvider, TargetVO> {
 
    public TargetVO process(EgovCompositeDataProvider eprovider) throws Exception {
        Object[] obj = eprovider.getMapItems();
 
        //배열에서 항목을 꺼내오는 로직	
        CustomerCredit vo1 = (CustomerCredit)obj[0];
        CustomerCredit vo2 = (CustomerCredit)obj[1];
 
 
        //비즈니스 로직 처리 가능 
        TargetVO vo = new TargetVO();
 
        if(vo1 !=null) {
            vo.setId(vo1.getId());
        }
        if(vo2 != null) {
            vo.setName(vo2.getName());
        }
        return vo;
    }
}

주의!

  • 배열에서 object를 꺼내오는 순서는 ItemList에 설정한 순서이며, 꺼내올 때 Object의 타입에 주의한다.
  • Processor에서 Writer로 넘겨줄 VO타입을 작성하는 부분에 주의한다.
  • ItemReader에서 읽어온 데이터의 길이가 항상 같지는 않기 때문에 읽어온 VO가 null이 아닐 때까지 처리되도록 null체크를 위한 분기문을 넣어준다.
  • 결과 VO는 반드시 1개가 되어야 하므로 위와 같이 처리할 수 없는 비즈니스 로직은 다른 방식으로 처리해야 한다.

참고자료

7.20 - History Management

배치작업 처리 중의 정보는 JobRepository의 JobInstance, JobParams, JobExecution, StepExecution, key-value 쌍으로 값을 보관할 수 있는 공간인 ExecutionContext에 저장 및 갱신되어 history를 관리한다.

History Management

개요

배치작업 처리 중의 정보는 JobRepository의 JobInstance, JobParams, JobExecution, StepExecution, key-value 쌍으로 값을 보관할 수 있는 공간인 ExecutionContext에 저장 및 갱신되어 history를 관리한다.

설명

JobInstance, JobParams, JobExecution, StepExecution, ExecutionContext 의 각각의 속성에 대해서 정리하였다.

BATCH_JOB_INSTANCE

JobInstance 속성설명
jobInstanceIdJobInstance를 식별하는 ID
versionJobInstance 의 수정 횟수
jobNameJob의 이름
jobKeyJobInstance를 구분 짓는 JobParameters의 serialization

BATCH_JOB_EXECUTION

JobExecution 속성설명
statusBatchStatus는 실행 상태를 나타내는 객체이다, 실행하는 동안에는 BatchStatus,STARTED, 실행이 실패한 경우 BatchStatus.FAILED, 실행이 성공적으로 종료됐을 경우 BatchStatus.COMPLETED가 된다.
startTimeExecution이 시작되는 현재 시스템 시간을 java.Util.Data로 저장
endTimeExecution의 성공/실패 여부와 관계없이 종료되는 현재 시스템 시간을 java.Util.Data로 저장
exitStatusExitStatus는 실행의 결과를 나타낸다. 호출자에게 반환될 exit code를 포함한다.
createTimeJobExecution이 최초 생성 된 현재 시스템 시간을 java.Util.Data로 저장
lastUpdatedJobExecution이 마지막으로 생성 된 현재 시스템 시간을 java.Util.Data로 저장
executionContextexecution간 지속돼야 할 모든 데이터를 포함하는 ‘프로퍼티 백’
failureExceptionsJob이 실행되는 동안 발생한 익셉션 리스트

BATCH_JOB_PARAMS

JobParams 속성설명
jobInstanceIdBATCH_JOB_INSTANCE 테이블의 jobInstanceId를 외래키로 지정
typeCd파라마터의 형식을 String으로 저장,null일 될 수 없음
keyName파라미터의 키
stringValString타입의 파마미터 값
dateValDate타입의 파마미터 값
longValLong타입의 파마미터 값
doubleValDouble타입의 파마미터의 값

BATCH_STEP_EXECUTION

StepExecution 속성설명
statusBatchStatus는 실행 상태를 나타내는 객체이다, 실행하는 동안에는 BatchStatus,STARTED, 실행이 실패한 경우 BatchStatus.FAILED, 실행이 성공적으로 종료됐을 경우 BatchStatus.COMPLETED가 된다.
startTimeExecution이 시작되는 현재 시스템 시간을 java.Util.Data로 저장
endTimeExecution의 성공/실패 여부와 관계없이 종료되는 현재 시스템 시간을 java.Util.Data로 저장
exitStatusExitStatus는 실행의 결과를 나타낸다. 호출자에게 반환될 exit code를 포함한다.
executionContextexecution간 지속돼야 할 모든 데이터를 포함하는 ‘프로퍼티 백’
readCount성공적으로 읽은 item 갯수
writeCount성공적으로 쓰인 item 갯수
commitCount해당 execution에서 커밋된 트랜젝션 횟수
rollbackCount롤백된 Step에 의해서 제어된 비즈니스 트랜젝션의 갯수
readSkipCount읽기 과정에서 실패 후, 스킵된 item 갯수
processSkipCount프로세스 과정에서 실패 후, 스킵된 item 갯수
filterCountItemProcessor에 의해 필터링 된 item 갯수
writeSkipCount쓰기 과정에서 실패 후, 스킵된 item 갯수

BATCH_JOB_EXECUTION_CONTEXT

JobExecutionContext 속성설명
jobExecutionIdBATCH_JOB_EXECUTION 테이블의 jobExecutionId를 외래키로 지정
shortContextSERIALIZED_CONTEXT의 문자열 버전
serializedContext전체 Context

BATCH_STEP_EXECUTION_CONTEXT

StepExecutionContext 속성설명
stepExecutionIdBATCH_STEP_EXECUTION 테이블의 stepExecutionId를 외래키로 지정
shortContextSERIALIZED_CONTEXT의 문자열 버전
serializedContext전체 Context

참고자료

7.21 - 동기/비동기 처리 예제

일괄(배치)처리 작업 수행 시, 작업처리가 종료될 때까지 대기하는 동기방식 처리와 작업처리의 종료를 Callback매커니즘을 이용하여 전달받는 비동기처리에 대한 예제를 보여준다.

동기/비동기 처리 예제

개요

일괄(배치)처리 작업 수행 시, 작업처리가 종료될 때까지 대기하는 동기방식 처리와 작업처리의 종료를 Callback매커니즘을 이용하여 전달받는 비동기처리에 대한 예제를 보여준다.

설명

설정

Launcher 설정

동기/비동기 처리 예제의 Launcher 설정파일인 skipSample-job-launcher-context.xml 을 확인한다.

Job 수행시, 동기와 비동기 방식으로 데이터를 처리할 수 있으며, 이 예제에서는 동기 처리가 기본값으로 설정되어 있다. 설정위치는 Launcher 설정파일의 jobLauncher 빈에서 taskExecutor 프로퍼티이며, 참조하는 값으로 다음 두 가지를 설정할 수 있다.

  • sync : 동기처리시 사용할 클래스 설정
  • async : 비동기처리시 사용할 클래스 설정
<bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
	<property name="jobRepository" ref="jobRepository" />
	<property name="taskExecutor" ref="sync"/>  <!-- 비동기시 ref="async" -->
</bean>

<!-- 동기 처리시  sync -->
<bean id="sync" class="org.springframework.core.task.SyncTaskExecutor" />

<!-- 비동기  처리시 async -->
<bean id="async" class="org.springframework.core.task.SimpleAsyncTaskExecutor" />

Job 설정

동기/비동기 처리 예제의 Job 설정 파일인 delegatingJob.xml을 확인한다.

동기/비동기 처리 예제를 위해 특별히 Job을 설정하는 내용은 없다. 이 예제에서 제공하는 Job의 상세 내용은 기존 업무 재사용 예제의 Job 설정과 같으므로 이를 참고한다.

<job id="delegateJob" xmlns="http://www.springframework.org/schema/batch">
	<step id="delegateStep1">
		<tasklet>
			<chunk reader="reader" writer="writer" commit-interval="3"/>
		</tasklet>
	</step>
</job>

<bean id="reader" class="org.springframework.batch.item.adapter.ItemReaderAdapter">
	<property name="targetObject" ref="delegateObject" />
	<property name="targetMethod" value="getData" />
</bean>

<bean id="writer" class="org.springframework.batch.item.adapter.PropertyExtractingDelegatingItemWriter">
	<property name="targetObject" ref="delegateObject" />
	<property name="targetMethod" value="processPerson" />
	<property name="fieldsUsedAsTargetMethodArguments">
		<list>
			<value>firstName</value>
			<value>address.city</value>
		</list>
	</property>
</bean>

<bean id="delegateObject" class="egovframework.brte.sample.common.domain.person.PersonService" />

Async Item Processor 구성

Item Processor를 비동기 처리하기 위해 Spring Batch에서 AsyncItemProcessor 서비를 지원한다.
AsyncItemProcessor 서비스를 이용한 설정은 아래와 같다.

<task:executor id="taskExecutor" pool-size="100"/>
<bean id="itemProcessorAsync" class="org.springframework.batch.integration.async.AsyncItemProcessor">
	<!-- delegate통해 실제 동작 할 Item Processor를 설정한다. -->
	<property name="delegate" ref="fixedLengthToFixedLengthJob.fixedLengthToFixedLengthStep.itemProcessor"/>
	<!-- Executor를 설정한다. -->	
	<property name="taskExecutor" ref="taskExecutor" />
</bean>

JunitTest 구성 및 수행

JunitTest 구성

sync-job-launcher-context설정과 delegatingJob설정으로 구성된 Junit Test를 수행한다. 이 때 배치가 수행되고, 관련된 내용을 확인할 수 있다.

✔ JunitTest 클래스의 구조는 배치실행환경 예제 Junit Test 설명을 참고한다.
✔ assertEquals(“COMPLETED”, jobExecution.getExitStatus().getExitCode()) : 배치수행결과가 COMPLETED 인지 확인한다.
✔ Thread.sleep(4000) : 비동기로 배치를 수행 시, DB에 배치상태(UNKNOWN)를 셋팅하고 DB연결이 종료되어 Job이 정상적으로 수행되더라도 종료상태(COMPLETED,FAILED)를 확인할 수 없다. 예제에서는 Job결과를 확인하기 위해 Thread를 적정시간동안 정지시켜 인위적으로 종료상태를 확인하도록 설정하였다.

@ContextConfiguration(locations = { "/egovframework/batch/sync-job-launcher-context.xml", 
			"/egovframework/batch/jobs/delegatingJob.xml", 
			"/egovframework/batch/job-runner-context.xml" })
public class EgovSyncDelegatingJobFunctionalTests {
	...
	@Test
	public void testLaunchJob() throws Exception {
		JobExecution jobExecution=null;
		try{
			   jobExecution =jobLauncherTestUtils.launchJob();
			   //Async 로 수행되는 경우 Exit Status는 UNKNOWN으로 설정 됨
			   assertEquals("UNKNOWN", jobExecution.getExitStatus().getExitCode());
			   Thread.sleep(4000); 
 
		   }catch (InterruptedException ie){
			   ie.printStackTrace();
		   }
		 assertTrue(personService.getReturnedCount() > 0);
		 assertEquals(personService.getReturnedCount(), personService.getReceivedCount()) ;
		 assertEquals("COMPLETED", jobExecution.getExitStatus().getExitCode());	
	}
}

JunitTest 수행

AsyncItemWriter

수행방법은 JunitTest 실행을 참고한다.

참고자료

7.22 - Listener (Pre/Post Processing)

Listener는 배치 작업의 각 단계(Job, Step, Chunk 등)에서 이벤트 설정을 통해 추가 구성을 할 수 있으며, 설정된 이벤트를 실행 중에 처리한다. JobListener는 Job의 라이프사이클 동안 다양한 이벤트를 감지하고, 사용자 정의 코드를 실행할 수 있도록 지원한다.

Listener (Pre/Post Processing)

개요

배치 수행시 Job을 구성하는 각 단계(Job, Step, Chunk, Read, Process, Write)에서 이벤트설정을 통해 다양한 추가구성을 할 수 있다. 이벤트는 Listener를 활용하여 설정하고, 배치 수행중 설정한 Listener를 접하게 되면 관련된 이벤트를 수행하게 된다.

설명

JobListener(Intercepting Job Execution)

Job 실행 과정에서, 사용자가 정의한 코드가 실행 될 수 있도록 Job의 라이프사이클에서 다양한 이벤트로 알려주는 것은 유용하다.SimpleJob는 적절한 시간에 JobListener를 호출하도록 한다.

public interface JobExecutionListener {
	void beforeJob(JobExecution jobExecution);
	void afterJob(JobExecution jobExecution);
}

JobListeners는 Job의 리스너들을 통해 SimpleJob에 추가 될 수 있다.

<job id="footballJob">
	<step id="playerload" parent="s1" next="gameLoad"/>
	<step id="gameLoad" parent="s2" next="playerSummarization"/>
	<step id="playerSummarization" parent="s3"/>
	<listeners>
		<listener ref="sampleListener"/>
	</listeners>
</job>

Job의 성공, 실패에 관계없이 afterJob이 호출되어야한다. 만약 성공과 실패의 결정이 필요하다면 JobExecution에서 얻을 수 있다.

public void afterJob(JobExecution jobExecution){
	if( jobExecution.getStatus() == BatchStatus.COMPLETED ){
		//job success
	}
	else if(jobExecution.getStatus() == BatchStatus.FAILED){
		//job failure
	}
}

✔ JobExecutionListener 인터페이스에 해당하는 Annotations

Annotations설명
@BeforeJobJob수행 전에 호출
@AfterJobJob수행 후에 호출

StepListener(Intercepting Step Execution)

StepExecutionListener

StepExecutionListener는 Step 실행에서 가장 일반적인 리스너이다. Step에 대한 정보를 Step 시작전과 종료후에 알려준다.

public interface StepExecutionListener extends StepListener {
	void beforeStep(StepExecution stepExecution);
	ExitStatus afterStep(StepExecution stepExecution);
}

afterStep의 반환 리턴타입인 ExitStatus는 청리스너에게 단계 완료시 반환되는 exit-code를 수정 할 수있는 기회를 준다.

이 인터페이스에 해당하는 주석입니다

✔ StepExecutionListener 인터페이스에 해당하는 Annotations

Annotations설명
@BeforeStepStep 수행 전에 호출
@AfterStepStep 수행 후에 호출

ChunkListener

Chunk는 트랜잭션 범위 내에서 아이템을 처리하는 것이다. 트랙잭션을 커밋하고, commit interval 단위로 Chunk를 커밋한다. ChunkListener는 Chunk 처리를 시작하기 전에 또는 성공적으로 완료 한 후 구현로직을 수행한다.

public interface ChunkListener extends StepListener {
	void beforeChunk();
	void afterChunk();
}

beforeChunk 메소드는 트랜잭션이 시작해서 ItemReader의 read 수행되기전에 불린다. 반대로, afterChunk 메소드는 rollback이 일어나지 않고 Chunk가 커밋된 후에 불린다.

✔ ChunkListener 인터페이스에 해당하는 Annotations

Annotations설명
@BeforeChunkChunk 수행 전에 호출
@AfterChunkChunk 수행 후에 호출

✔ TaskletStep 수행같은 Chunk 선언이 없는 경우에는 ChunListener을 적용할 수 없다.

ItemReadListener

Skip 로직 설명에서, 나중에 처리 할 수 있도록 Skip로그를 기록할 수 있다고 언급했다. read 오류의 경우, ItemReaderListener 가 이 작업을 수행한다.

public interface ItemReadListener<T> extends StepListener {
	void beforeRead();
	void afterRead(T item);
	void onReadError(Exception ex);
}

beforeRead 메소드는 ItemReader에서 read 수행 전에 호출된다.
afterRead 메소드는 read 수행이 성공했을때 호출되고, 읽은 item을 전달한다.
Read 수행중 오류가 발생하는 경우 onReadError 메서드가 호출된다. 기록을 위해 발생한 예외정보가 전달된다.

✔ ItemReadListener인터페이스에 해당하는 Annotations

Annotations설명
@BeforeReadRead수행 전에 호출
@AfterReadRead수행 후에 호출
@OnReadErrorRead 수행중 오류발생시 호출

ItemProcessListener

ItemReadListener 처럼, item 처리에서도 리스너가 있다.

public interface ItemProcessListener<T, S> extends StepListener {
	void beforeProcess(T item);
	void afterProcess(T item, S result);
	void onProcessError(T item, Exception e);
}

beforeProcess 메소드는 ItemProcessor의 process 과정 이전에 호출이 되고, 처리되는 item 을 전달한다.
afterProcess 메소드는 item 이 성공적으로 처리된 후에 호출된다.
만약 처리과정에서 에러가 발생한다면, onProcessError가 호출되는데 예외정보와 item을 전달하므로 기록을 남길 수 있다.

✔ ItemProcessListener 인터페이스에 해당하는 Annotations

Annotations설명
@BeforeProcessProcess 수행 전에 호출
@AfterProcessProcess 수행 후에 호출
@OnProcessErrorProcess 수행중 오류발생시 호출

ItemWriteListener

ItemWriteListener로 item을 쓰는 과정에서 리스너를 호출할 수 있다.

public interface ItemWriteListener<S> extends StepListener {
	void beforeWrite(List<? extends S> items);
	void afterWrite(List<? extends S> items);
	void onWriteError(Exception exception, List<? extends S> items);
}

beforeWrite 메소드는 ItemWriter에서 write 수행 전에 호출되고 쓰여진 item을 전달한다.
afterWrite 메소드는 write 수행이 성공했을때 호출되고, 읽은 아이템을 전달한다.
write 수행중 오류가 발생하는 경우 onWriteError 메소드가 호출된다. 기록을 위해 발생한 예외정보와 item들이 리스트형식으로 전달된다.

✔ ItemWriteListener 인터페이스에 해당하는 Annotations

Annotations설명
@BeforeWriteWrite 수행 전에 호출
@AfterWriteWrite 수행 후에 호출
@OnWriteErrorWrite 수행중 오류 발생시 호출

SkipListener

itemReadListener, ItemProcessListener, ItemWriteListner 에서 에러발생을 알려주는 메카니즘을 제공하지만, item 처리가 Skip 될 경우에는 아무도 알려주지 않는다. 이를 위해 Skip된 item을 추적하는 인터페이스가 있다.

public interface SkipListener<T,S> extends StepListener {
	void onSkipInRead(Throwable t);
	void onSkipInProcess(T item, Throwable t);
	void onSkipInWrite(S item, Throwable t);
}

onSkipInRead 메소드는 read 수행중 Skip 발생할경우 언제나 호출된다. 주의할 점은 한 번 이상 Skip 되었기에 롤백은 동일한 항목이 등록될 수 있다는 것을 알려야 한다.
onSkipInWrite 메소드는 write 수행중 Skip 이 발생한 경우 호출되는데, 여기서 해당 item은 성공적으로 read 되었기 때문에 item 그 자체를 인수로 제공한다.

✔ SkipListener 인터페이스에 해당하는 Annotations

Annotations설명
@OnSkipInReadRead 수행중 Skip 발생시 호출
@OnSkipInWriteWrite 수행중 Skip 발생시 호출
@OnSkipInProcessProcess 수행중 Skip 발생시 호출
SkipListeners and Transactions

SkipListener에 대한 가장 일반적인 사용 사례 중 하나는 다른 일괄 처리나 사람이 직접 처리할 때 사용할 수 있도록 Skip된 item을 기록하는 것이다.

✔ 트랜잭션에서 롤백 될 수 있는 경우가 많기 때문에 스프링에서는 다음 두가지를 보장한다.

1. 적절한 Skip 방법은(오류에 따라) 항목 당 한 번만 호출된다.
2. SkipListener는 항상 트랜잭션 커밋 전에 호출된다. 그러므로 리스너에 의해 호츨된 트랜잭션 자원은 ItemWriter에서 실패하여 롤백되지 않는다.

관련 예시

  • 어노테이션 사용
public class EventNoticeListener {
	@Autowired
	EgovEmailEventNoticeTrigger egovEmailEventNoticeTrigger;
 
 
	// Job 수행완료 후 수행
	@AfterJob
	public ExitStatus sendJobNotice(JobExecution jobExecution) {
 
		egovEmailEventNoticeTrigger.invoke(jobExecution);
		...
	}
 
	 // Step 수행완료 후 수행
	@AfterStep
	public ExitStatus sendStepNotice(StepExecution stepExecution) {
 
		egovEmailEventNoticeTrigger.invoke(stepExecution);
		...
	}
 
	// Read 중 Error 발생시 수행
	@OnReadError
	public void sendErrorNotice(Exception e) {
 
		egovEmailEventNoticeTrigger.invoke(e);
		...
	}
}

작업 전후처리 관리 (EgovPre/PostProcessor)

전자정부 표준프레임워크에서는 스프링에서 제공하는 다양한 Listener를 배치작업의 구성요소(Job, Step, Chunk)별로 나누고 각 단계의 전/후로 나누어, 클래스 이름만으로 독립적인 역할을 명확히 알 수 있는 Processor를 제공한다. Processor 들은 Job 설정파일의 <listener>가 호출하며, 각각의 Processor가 호출되는 위치는 다음 그림을 참조한다.

✔ 그림의 EgovSampleXXXProcessor는 예시 클래스이며, 전자정부 표준프레임워크에서 제공하는 Processor를 상속받아 구현된 클래스이다.

image

Job Processor

종류
클래스명제공메소드명파라미터설명
EgovJobPreProcessorbeforeJob()JobExecutionJob 단계 이전에 호출
EgovJobPostProcessorafterJob()JobExecutionJob 단계 이후에 호출
public class EgovJobPreProcessor extends JobExecutionListenerSupport {
	/**
	 * Job 수행 이전에 호출되는 부분
	 */
	public void beforeJob(JobExecution jobExecution) {
 
	}
}
public class EgovJobPostProcessor extends JobExecutionListenerSupport {
	/**
	 * Job 수행 이후에 호출되는 부분 
	 */
	public void afterJob(JobExecution jobExecution) {
 
	}
}
설정

위 클래스를 상속받아 사용자가 정의한 Job Processor는 설정파일에서 <listeners>를 이용해 다음과 같은 위치에 설정한다.

<job id="ProcessorJob" xmlns="http://www.springframework.org/schema/batch">
	<listeners>
		<listener ref="jobListener" />
	</listeners>
	<step id="ProcessorStep">
		<tasklet>
			<chunk reader="itemReader" writer="itemWriter" commit-interval="2"/>
		</tasklet>
	</step>
</job>
 
<bean id="jobListener" class="사용자가 정의한 Job Processor 클래스" />

Step Processor

종류
클래스명제공메소드명파라미터설명
EgovStepPreProcessorbeforeStep()StepExecutionStep 단계 이전에 호출
EgovStepPostProcessorafterStep()StepExecutionStep 단계 이후에 호출
public class EgovStepPreProcessor<T, S> extends StepListenerSupport<T, S> {
	/**
	 * Step 수행 이전에 호출되는 부분
	 */
	public void beforeStep(StepExecution stepExecution) {
 
	}
}
public class EgovStepPostProcessor<T, S> extends StepListenerSupport<T, S> {
	/**
	 * Step 수행 이후에 호출되는 부분
	 */
	public ExitStatus afterStep(StepExecution stepExecution) {
		return null;
	}
}
설정

위 클래스를 상속받아 사용자가 정의한 Step Processor는 설정파일에서 <listeners> 를 이용해 다음과 같은 위치에 설정한다.

<job id="ProcessorJob" xmlns="http://www.springframework.org/schema/batch">
	<step id="ProcessorStep">
		<tasklet>
			<chunk reader="itemReader" writer="itemWriter" commit-interval="2"/>
		</tasklet>
		<listeners>
			<listener ref="stepListener" />
		</listeners>
	</step>
</job>
 
<bean id="stepListener" class="사용자가 정의한 Step Processor 클래스" />

Chunk Processor

종류
클래스명제공메소드명파라미터설명
EgovChunkPreProcessorbeforeChunk()없음Chunk 단계 이전에 호출
EgovChunkPostProcessorafterChunk()없음Chunk 단계 이후에 호출
public class EgovChunkPreProcessor extends ChunkListenerSupport {
	/**
	 * Chunk 수행 이전에 호출되는 부분
	 */
	public void beforeChunk() {
 
	}
}
public class EgovChunkPostProcessor extends ChunkListenerSupport {
	/**
	 * Chunk 수행 이후에 호출되는 부분
	 */
	public void afterChunk() {
 
	}
}
설정

위 클래스를 상속받아 사용자가 정의한 Chunk Processor는 설정파일에서 <listeners> 를 이용해 다음과 같은 위치에 설정한다.

<job id="ProcessorJob" xmlns="http://www.springframework.org/schema/batch">
	<step id="ProcessorStep">
		<tasklet>
			<chunk reader="itemReader" writer="itemWriter" commit-interval="2">
				<listeners>
					<listener ref="chunkListener" />
				</listeners>
			</chunk>	
		</tasklet>
	</step>
</job>
 
<bean id="chunkListener" class="사용자가 정의한 Chunk Processor 클래스" />

사용예시

작업 전후처리 예제

참고자료

http://static.springsource.org/spring-batch/reference/html/configureJob.html#interceptingJobExecution
http://static.springsource.org/spring-batch/reference/html/configureStep.html#interceptingStepExecution

7.23 - 병행처리

배치 작업에서 병렬처리를 통해 Job의 구성요소들을 여러 쓰레드로 분산 실행하여 빠르고 효율적으로 작업을 완료할 수 있다. 스프링 배치에서는 멀티쓰레드, Parallel 방식, 파티셔닝 방식 등 다양한 병렬처리 방식을 지원하며, 멀티쓰레드는 Step의 에 TaskExecutor를 추가해 간단히 구현할 수 있다.

병행처리

개요

대용량 데이터를 처리하는 배치수행에서 병렬처리를 이용하면, Job의 구성요소들이 여러 쓰레드 분산수행되어 빠른 시간 내에 효율적으로 작업을 완료할 수 있다. 스프링 배치에서 병렬처리 방식은 실행 유형별로 멀티쓰레드 방식, Parallel 방식, 파티셔닝 방식 등이 있다.

설명

멀티쓰레드(Multi-threaded Step)

병렬처리를 시작하는 간단한 방법은 Step 구성요소중 <tasklet> 속성에 TaskExecutor를 추가하는 것이다.

<step id="loading">
    <tasklet task-executor="taskExecutor">...</tasklet>
</step>

TaskExecutor 예제에서 TaskExecutor 인터페이스를 구현하기 위해 빈을 정의한다. TaskExecutor 는 스프링 인터페이스 표준이므로 상세한 내용은 스프링 가이드를 참고한다. 가장 간단한 멀티쓰레드 TaskExecutor 는 SimpleAsyncTaskExecutor 이다. 위 Step 구성으로 수행한 결과 각 청크단위의 reading, processing, writing 과정이 분리된 쓰레드에서 수행된다. 즉, 처리시 순서를 보장하지 않으며 Chunk는 단일 쓰레드 수행과 비교해 item 들이 연속적이지 않다.(commit-Interval의 영향으로 Chunk 내의 순서는 같을 수 있다.)

✔ 쓰레드 수는 기본값으로 4가 설정되어 있으나, 필요하다면 다음처럼 증가시켜 사용한다.

<step id="loading"> 
	<tasklet task-executor="taskExecutor" throttle-limit="20">
		...
	</tasklet>
</step>

✔ DataSource 처럼 Step에서 사용되는 풀이 리소스들에 의해 대체될 수 있다. 그러므로 Step에서 병행처리 되는 쓰레드 수를 원하는 만큼 최대한 풀을 설정해야 한다.

관련 예제

멀티쓰레드 예제

Parallel Steps

병행처리가 필요한 응용프로그램 로직은 서로 다른 책임으로 분할될 뿐만 아니라, 각 단계에서 할당되면 그것이 한 프로세스에서 병행처리가 될 수 있다. Parallel Step 수행은 사용하고 구성하기 쉽다. 예를 들어, step3 와 병행처리할 스텝들(step1, step2)은 다음처럼 흐름을 설정하면 된다.

<job id="job1">
    <split id="split1" task-executor="taskExecutor" next="step4">
        <flow>
            <step id="step1" parent="s1" next="step2"/>
            <step id="step2" parent="s2"/>
        </flow>
        <flow>
            <step id="step3" parent="s3"/>
        </flow>
    </split>
    <step id="step4" parent="s4"/>
</job>
 
<beans:bean id="taskExecutor" class="org.spr...SimpleAsyncTaskExecutor"/>

task-executor 속성은 각각의 흐름을 실행하는데 필요한 TaskExecutor 구현을 지정하기 위해 사용한다. 기본 설정은 SyncTaskExecutor 이고, 비동기로 실행하려면 이 설정을 AsyncTaskExecutor 로 변경해 주어야 한다.

✔ 각 작업 분할은 최종 종료 상태로 통합되기 전에 모두 완료하도록 구성해야 한다. 아래 그림처럼 분리된 flow들이 모두 완료해야만 다음 step으로 진행가능하다.

parallelstep1

관련 예제

Parallel 예제

파티셔닝(Partitioning)

스프링배치는 Step의 파티셔닝 수행을 원격으로 파티셔닝하기 위하여 SPI를 제공한다. 다음 수행 패턴을 그림으로 표현했다.

partition-overview

Job은 왼쪽부터 Step의 흐름대로 진행된다. Step들 중에서 하나는 마스터 라벨로 지정되어 있다. 이 그림에서 슬레이브 라벨들은 Step의 인스턴스로 식별되고, 그 결과가 마스터의 결과로 귀속된다. 슬레이브는 전형적으로 원격서비스로 이루어지며 로칼쓰레드로 전달된다. JobRepository 의 스프링배치 meta-data에서 각각의 슬레이브가 Job에서 각각 한번 수행되는 것을 보장한다.

스프링배치의 SPI는 Step의 특별한 구현(PartitionStep)으로 구성되어있다. PartitionHandler와 StepExecutionSplitter 라는 두개의 인터페이스가 있고 이 역할은 아래 그림을 참고한다.

partition-flow

오른쪽의 Step은 잠재적으로 여러 객체를 갖고 있고 각 역할을 수행하는 remote 슬레이브이다. Partition Step은 다음과 같이 구성되어 있다.

<step id="step1.master">
    <partition step="step1" partitioner="partitioner">
        <handler grid-size="10" task-executor="taskExecutor"/>
    </partition>
</step>

✔ 멀티쓰레드 스텝의 throttle-limit 속성과 유사하게 grid-size 속성이 있어서, 이것이 각 Step의 요청이 포화상태가 되는 것을 방지한다.

PartitionHandler

PartitionHandler 는 Remote 환경이나 Grid 환경의 구조를 알고있는 컴포넌트이다. 이것은 DTO 같은 포맷으로 감싸 StepExecution을 원격에 있는 Step에 보낼 수 있다. 여기에서는 입력데이터들이 어떻게 나누어지는지, 멀티 Step 수행결과들이 어떻게 합쳐지는지 알 필요가 없다. 하지만 스프링 배치는 TaskExecutor 전략을 이용하여 분리된 쓰레드에서 Step들을 수행시키는 유용한 PartitionHandler 도 제공한다. 이런 구현체를 TaskExecutorPartitionHandler라고 하는데 XML 로 구성된 Step에 기본값으로 정해져 있다. 또한, 다음처럼 구성할 수 있다.

<step id="step1.master">
    <partition step="step1" handler="handler"/>
</step>
 
<bean class="org.spr...TaskExecutorPartitionHandler">
    <property name="taskExecutor" ref="taskExecutor"/>
    <property name="step" ref="step1" />
    <property name="gridSize" value="10" />
</bean>

✔ gridSize는 새로 생성할 분리된 Step의 수를 의미하는데, TaskExecutor에서 쓰레드풀 갯수와 같다. gridSize는 사용가능한 쓰레드 수 이상으로 설정이 될 수 있는데, 이 경우 작은 수가 적용이 된다.

Partitioner

Partitioner는 새로운 Step Execution을 위한 입력 파라미터와 같은 유사한 역할을 한다.(재시작에 대해 걱정할 필요가 없다.) 아래와 같은 인터페이스가 있으며 메소드는 partition 메소드 하나뿐이다.

public interface Partitioner {
    Map<String, ExecutionContext> partition(int gridSize);
}

여기서 메소드의 리턴값은 ExecutionContext 타입과 String 타입의 유일한 이름인 stepExecution이다. 여기서 이름은 파티션된 StepExecution의 Step 이름으로 이후에 배치의 meta data에서 보여준다. ExecutionContext 는 이름, 값으로 구성된 여러 쌍들이 담긴 가방이며 기본키, line 수, 입력 파일의 위치등의 정렬을 포함할 수도 있다.

StepExecution 들의 이름은 Job의 StepExecution이 서로 달라야 한다. 가장좋은 방법은 prefix+suffix 의 형태로 사용자를 위한 의미있는 이름을 만드는 것이다. 여기서 prefix는 실행되는 Step의 이름이고, suffix는 카운터이다. SimplePartitioner가 이와 같은 형태의 StepExecution 이름을 지정한다.

Step에 입력리소스 등록하기(Binding Input Data to Steps)

Step이 동일한 구성을 갖는 것은 PartitionHandler에 의해 실행하는 Step과 ExecutionContext에서 런타임에 바인딩하기 위한 입력 매개 변수에 매우 효율적이다. 이것은 스프링 배치의 기능인 Step Scope기능과 비슷하다.

예를 들어, Partitioner 가 fileName 키 속성을 갖고 있는 ExecutionContext 인스턴스를 생성한다면, 각 Step들은 서로다른 파일들을 바라보게 되고 출력결과는 다음처럼 다.

Step Execution Name (key)ExecutionContext (value)
filecopy:partition0fileName=/home/data/one
filecopy:partition1fileName=/home/data/two
filecopy:partition2fileName=/home/data/three

관련 예제

파티셔닝 예제

참고자료

7.24 - Code Base Exception

배치 처리 시 EgovBatchException을 통해 Code 기반으로 에러를 처리할 수 있으며, 이를 사용하려면 먼저 데이터베이스 에러 코드 관리 테이블과 에러 코드 데이터를 등록해야 한다. 이 서비스를 통해 에러 처리의 효율성을 높일 수 있다.

Code Base Exception

개요

배치 처리시 Code 기반으로 에러를 처리 할 수 있도록 EgovBatchException를 통해서 지원한다. 데이터베이스 에러코드관리 테이블을 등록과 에러코드 데이터를 등록이 선행 되야지만 해당 서비스를 사용가능하다.

설명

Code Base Exception 데이터베이스 설정

C REATE TABLE BATCH_EXCEPTION_MESSAGE  (
	EX_ID BIGINT NOT NULL PRIMARY KEY,
	EX_KEY VARCHAR(255) NOT NULL,
	EX_MESSAGE VARCHAR(2500) NOT NULL
);
I NSERT INTO BATCH_EXCEPTION_MESSAGE VALUES(1,'EGOVBATCH000001','배치실행 중 업무 관련 에러가 발생 하였습니다.');
I NSERT INTO BATCH_EXCEPTION_MESSAGE VALUES(2,'EGOVBATCH000002','배치실행 중 알수 없는 오류가 발생 하였습니다.');
  • 방화벽 정책상 [C REATE] 문자를 space 처리 하였습니다. space 문자를 제거 하시면 됩니다.
  • 방화벽 정책상 [I NSERT] 문자를 space 처리 하였습니다. space 문자를 제거 하시면 됩니다.
  • 방화벽 정책상 [I NTO] 문자를 space 처리 하였습니다. space 문자를 제거 하시면 됩니다.

Code Base Exception 사용

에러처리 생성자 생성자 파리미터 데이터베이스소스, 에러코드를 사용하여 에러처리를 할 수 있습니다.


try{
	...
}catch(Exception e){
	throw new EgovBatchException(dataSource,"EGOVBATCH000001");
        //Sql 설정시 EgovBatchException 생성자 파리미터 추가
        //throw new EgovBatchException(dataSource,"EGOVBATCH000001","SELECT EX_MESSAGE FROM BATCH_EXCEPTION_MESSAGE WHERE EX_KEY = ?");
}

7.25 - 센터 컷(CenterCut)

전자정부 표준프레임워크에서의 큐(Queue)를 사용하여 대용량 데이터 처리를 위해 센터 컷 방식의 배치 작업수행을 위한 가이드를 제공한다.

센터 컷(CenterCut) 소개

개요

전자정부 표준프레임워크에서의 큐(Queue)를 사용하여 대용량 데이터 처리를 위해 센터 컷 방식의 배치 작업수행을 위한 가이드를 제공한다.

센터컷 가이드 구조

  • Unordered List Item기본적으로 센터컷의 구조는 큐(Queue)를 이용하는 부분을 제외하고는 배치 프로그램과 유사하다.

  • Unordered List Item처음 ItemReader를 사용하여 데이터를 읽고 큐에 넣은 Center-Cut Reading Step과, 읽어온 데이터를 가공 후 QueueSender를 통해 Queue에 넣는 구조이다.

    centercut-explain1

  • Center-Cut Process Step은 큐에서 들어온 데이터를 읽고 처리 모듈(Busineess Proc)를 활용하여 데이터를 처리하는 구조이다.

    centercut-queueproc1

[참고] QueueSender, QueueReciever만 센터컷을 위해 추가되는 모듈을 가이드하며, 나머지는 배치와 동일하다.

센터 컷의 가이드 기본 구성

  1. Queue를 사용한 센터 컷 배치 처리를 위하여 Apache ActiveMQ를 활용한다. 현재 가이드의 ActiveMQ의 버전은 apache-activemq-5.15.1이며, 관련 프로그램 및 활용 방법은 아래의 ActiveMQ에서 확인할 수 있다. Apache ActiveMQ ActiveMQ를 설치 및 실행 후, http:localhost:8161에서 실행 확인을 할 수 있으며, 관리자 계정(admin/admin) 로그인 후 Queue 메뉴에서 현재 큐의 사황을 확인 할 수 있다.

    activemq

  2. 구성 된 ActiveMQ를 활용하여 1개의 배치 Job과 2개의 Step(QueueSender, QueueProc)를 처리하는 가이드 예제를 활용한다.

가이드 예제의 기본 구성

  • 큐서버(ActiveMQ)의 접속 Url 등록 : batch.properties에 batch.queue.url에 기술 (예:127.0.0.1:61616)
  • 배치 Job 설정 : centerCutJob.xml에 1개의 Job과 2개의 Step의 빈 설정
<job id="centerCutJob" parent="eGovBaseJob" xmlns="http://www.springframework.org/schema/batch">
	<step id="stepQueueSender" next="stepQueueProc">
		<tasklet ref="taskletQueueSender" />
	</step>
	<step id="stepQueueProc">
		<tasklet ref="taskletQueueProc" />
	</step>
</job>

<bean id="taskletQueueSender" class="egovframework.rte.bat.centercut.TaskletQueueSender" scope="step">
</bean>

<bean id="taskletQueueProc" class="egovframework.rte.bat.centercut.TaskletQueueProc" scope="step">
</bean>
  • 테스트 파일 : EgovCenterCutJobRunnerTest.java파일을 실행하여 테스트를 진행한다.

QueueSender

TaskletQueueSender를 통하여 10,000개의 임의 데이터를 전송하고 endSender를 통하여 end message를 전송한다. (총 전송되는 메시지는 10,000개 메시지와 1개의 end message를 전송)

  • 10,000개 임의 데이터 전송
public RepeatStatus execute(StepContribution contribution,
			ChunkContext chunkContext) throws Exception {
 
	LOGGER.debug("TaskletQueueSender execute START ===");

	QueueSenderFactory qf = new QueueSenderFactory("test_queue");
	TextMessage txMessage = qf.getMessage();
	MessageProducer sender = qf.getSender();
	qf.setRemove(true);

	for(int i=0; i<10000; i++){
		LOGGER.debug("Send Value : " + i );
		txMessage.setText(String.valueOf(i));
		sender.send(txMessage);
		senderCount++;
	}

	qf.endSender(sender);
	// * 오류로 주석처리
	qf.close();

	// LOGGER를 사용하는 것과 달리 MessageFormatter를 사용할 경우 Log 레벨과 상관 없이 결과를 로그에 기록함.
	// 따라서 개발/운영계에 로그레벨이 달라지는 경우에도 출력을 보장함
	LOGGER.debug("########## Center-Cut Result ##########");
	LOGGER.debug("## Sender  Count : " + senderCount);
	LOGGER.debug("########################################");

	return RepeatStatus.FINISHED;
}
  • qf.endSender를 통하여 end message 전송
public void endSender(MessageProducer sender) throws Exception {
	TextMessage message = session.createTextMessage();
	setRemove(true);
	message.setText("End Of QUEUE"); //end message
	sender.send(message);
 
}
  • 전송 결과 (console출력)
xxxx-xx-xx 11:20:16,566 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9989
xxxx-xx-xx 11:20:16,566 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9990
xxxx-xx-xx 11:20:16,566 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9991
xxxx-xx-xx 11:20:16,566 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9992
xxxx-xx-xx 11:20:16,566 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9993
xxxx-xx-xx 11:20:16,566 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9994
xxxx-xx-xx 11:20:16,566 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9995
xxxx-xx-xx 11:20:16,566 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9996
xxxx-xx-xx 11:20:16,567 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9997
xxxx-xx-xx 11:20:16,567 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9998
xxxx-xx-xx 11:20:16,567 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] Send Value : 9999
xxxx-xx-xx 11:20:16,570 DEBUG [org.apache.activemq.transport.tcp.TcpTransport] Stopping transport tcp:///127.0.0.1:61616
xxxx-xx-xx 11:20:26,574 DEBUG [egovframework.rte.bat.queue.QueueRemover] >>>>> amqJmxUrl = service:jmx:rmi:///jndi/rmi://localhost:1099/jmxrmi
xxxx-xx-xx 11:20:26,725 DEBUG [egovframework.rte.bat.queue.QueueRemover] >>>>> init MBeanServerConnection connection = javax.management.remote.rmi.RMIConnector$RemoteMBeanServerConnection@3bcd426c
xxxx-xx-xx 11:20:26,725 DEBUG [egovframework.rte.bat.queue.QueueRemover] >>>>> queue remove - clientServiceName = org.apache.activemq:type=Broker,brokerName=localhost
xxxx-xx-xx 11:20:26,728 DEBUG [egovframework.rte.bat.queue.QueueRemover] >>>>> ConsumerCount = 0
xxxx-xx-xx 11:20:26,728 DEBUG [egovframework.rte.bat.queue.QueueRemover] >>>>> queue remove - clientServiceName = org.apache.activemq:type=Broker,brokerName=localhost
xxxx-xx-xx 11:20:26,728 DEBUG [egovframework.rte.bat.queue.QueueRemover] >>>>> queue remove - queueName = test_queue
xxxx-xx-xx 11:20:26,728 DEBUG [egovframework.rte.bat.queue.QueueRemover] >>>>> queue remove - operationName = removeQueue
xxxx-xx-xx 11:20:26,738 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] ########## Center-Cut Result ##########
xxxx-xx-xx 11:20:26,738 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] ## Sender  Count : 10000
xxxx-xx-xx 11:20:26,738 DEBUG [egovframework.rte.bat.centercut.TaskletQueueSender] ########################################

Queue Process

또한, 전자정부 표준프레임워크는 TaskletQueueProc를 통하여 10,000개의 데이터를 전송받아 처리하면서, end message를 통하여 배치 처리를 종료한다. (총 10,000개 메시지를 처리하고 1개의 end message를 통하여 완료한다.)

  • 10,000개를 Queue에서 받기
public RepeatStatus execute(StepContribution contribution,
	ChunkContext chunkContext) throws Exception {
 
	LOGGER.debug("TaskletQueueProc execute START ===");
 
	QueueReceiverFactory qf = new QueueReceiverFactory("test_queue");
	MessageConsumer receiver = qf.getReceiver();
 
	LOGGER.debug("=====>>>>> Start");
 
	while (true){
		textMessage = (TextMessage)receiver.receive();
		if(textMessage.getText().equals("End Of QUEUE")){
			LOGGER.debug("**********Receive End Message: " + textMessage.getText());
			qf.sessionCommit();
			break;
		}
		LOGGER.debug("Receive Message: " + textMessage.getText());
		qf.sessionCommit();
		recieveCount++;
	}
	qf.close();
 
	LOGGER.debug("########## Center-Cut Result ##########");
	LOGGER.debug("## Recieve  Count : " + recieveCount);
	LOGGER.debug("########################################");
 
	return RepeatStatus.FINISHED;
}
  • 전송 결과 (console출력)
xxxx-xx-xx 11:45:27,042 DEBUG [egovframework.rte.bat.centercut.TaskletQueueProc] Receive Message: 9997
xxxx-xx-xx 11:45:27,042 DEBUG [org.apache.activemq.ActiveMQSession] ID:DESKTOP-NV1780L-60402-1518144303940-3:1:1 Transaction Commit :TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:9998
xxxx-xx-xx 11:45:27,042 DEBUG [org.apache.activemq.TransactionContext] Commit: TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:9998 syncCount: 1
xxxx-xx-xx 11:45:27,043 DEBUG [org.apache.activemq.TransactionContext] Begin:TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:9999
xxxx-xx-xx 11:45:27,043 DEBUG [egovframework.rte.bat.centercut.TaskletQueueProc] Receive Message: 9998
xxxx-xx-xx 11:45:27,043 DEBUG [org.apache.activemq.ActiveMQSession] ID:DESKTOP-NV1780L-60402-1518144303940-3:1:1 Transaction Commit :TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:9999
xxxx-xx-xx 11:45:27,043 DEBUG [org.apache.activemq.TransactionContext] Commit: TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:9999 syncCount: 1
xxxx-xx-xx 11:45:27,044 DEBUG [org.apache.activemq.TransactionContext] Begin:TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:10000
xxxx-xx-xx 11:45:27,044 DEBUG [egovframework.rte.bat.centercut.TaskletQueueProc] Receive Message: 9999
xxxx-xx-xx 11:45:27,044 DEBUG [org.apache.activemq.ActiveMQSession] ID:DESKTOP-NV1780L-60402-1518144303940-3:1:1 Transaction Commit :TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:10000
xxxx-xx-xx 11:45:27,044 DEBUG [org.apache.activemq.TransactionContext] Commit: TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:10000 syncCount: 1
xxxx-xx-xx 11:45:27,045 DEBUG [org.apache.activemq.TransactionContext] Begin:TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:10001
xxxx-xx-xx 11:45:27,045 DEBUG [egovframework.rte.bat.centercut.TaskletQueueProc] **********Receive End Message: End Of QUEUE
xxxx-xx-xx 11:45:27,045 DEBUG [org.apache.activemq.ActiveMQSession] ID:DESKTOP-NV1780L-60402-1518144303940-3:1:1 Transaction Commit :TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:10001
xxxx-xx-xx 11:45:27,045 DEBUG [org.apache.activemq.TransactionContext] Commit: TX:ID:DESKTOP-NV1780L-60402-1518144303940-3:1:10001 syncCount: 1
xxxx-xx-xx 11:45:27,050 DEBUG [org.apache.activemq.ActiveMQMessageConsumer] remove: ID:DESKTOP-NV1780L-60402-1518144303940-3:1:1:1, lastDeliveredSequenceId:60066
xxxx-xx-xx 11:45:27,052 DEBUG [org.apache.activemq.transport.tcp.TcpTransport] Stopping transport tcp:///127.0.0.1:61616
xxxx-xx-xx 11:45:27,052 DEBUG [egovframework.rte.bat.centercut.TaskletQueueProc] ########## Center-Cut Result ##########
xxxx-xx-xx 11:45:27,052 DEBUG [egovframework.rte.bat.centercut.TaskletQueueProc] ## Recieve  Count : 10000
xxxx-xx-xx 11:45:27,052 DEBUG [egovframework.rte.bat.centercut.TaskletQueueProc] ########################################

참고자료

7.26 - 이벤트알림 템플릿 관리

전자정부 표준프레임워크의 배치 수행중 특정시점에 이벤트가 발생하는 경우 EventNoticeTrigger 인터페이스를 활용하여 SMS, Email 등을 통해 정보를 전달할 수 있는 추상화된 외부알림 access 관리기능이다.

이벤트알림 템플릿 관리

개요

전자정부 표준프레임워크의 배치 수행중 특정시점에 이벤트가 발생하는 경우 EventNoticeTrigger 인터페이스를 활용하여 SMS, Email 등을 통해 정보를 전달할 수 있는 추상화된 외부알림 access 관리기능이다.

설명

EventNoticeTrigger

EventNoticeTrigger 는 이벤트 알림 기능을 사용하도록 추상화된 인터페이스이다.

메소드는 트리거를 호출하는 invoke 메소드를 파라메터 타입별로 구성하였고, 모든 메소드의 리턴타입은 void 이다.

메소드파라메터설명
invoke()없음파라메터 없이 이벤트호출 ‌로직 구현
invoke(StepExecution)StepExecution파라메터인 StepExecution 정보를 활용하는 로직 구현
invoke(JobExecution)JobExecution파라메터인 JobExecution 정보를 활용하는 로직 구현
invoke(Exception)Exception파라메터인 Execption 정보를 활용하는 로직 구현

EgovEventNoticeTrigger

EgovEventNoticeTrigger 클래스는 EventNoticeTrigger 인터페이스를 상속받은 형태로, 파라메터 타입별로 이벤트 알림 기능(Email전송, SMS전송)을 호출하기 위한 invoke 메소드를 갖고 있다. 모든 메소드의 리턴타입은 void로 로직수행 후 반환해야 하는 값이 없고, 이는 배치 고유의 수행과정과 별도로 동작하는 부분이다.

사용자는 EgovEventNoticeTrigger 를 상속받아 각각의 파라메터 타입별로 재정의하여 사용한다.

eventnotice_interface

설정

‘프로세서(리스너) 설정’ 부분과 ‘트리거 설정’ 부분으로 나뉜다.

  • 프로세서(리스너) 설정
  1. Job 설정에서 이벤트를 호출할 수 있도록 리스너 설정을 하고 관련 클래스를 빈으로 등록한다.
<job id="eventNoticeTriggerJob" xmlns="http://www.springframework.org/schema/batch">
	<step id="eventNoticeTriggerStep1">
		<tasklet>
			<chunk reader="itemReader" writer="itemWriter" commit-interval="2" />
		</tasklet>
		<listeners>
			<listener ref="EventNoticeCallListener" />
		</listeners>
	</step>
</job>
 
<bean id="EventNoticeCallListener" class="egovframework.brte.sample.example.listener.EgovEventNoticeCallProcessor" />
  1. 위에서 설정한 프로세서를 구현한다. 작업 전후처리 관리의 프로세서를 상속받아 사용자가 원하는 시점 (아래 예시는 Step수행 후에 호출되는 메소드)에서 invoke 메소드를 호출하도록 구현한다. 메소드에 invoke 메소드는 EgovEventNoticeTrigger을 상속받아 재정의한 클래스(‘트리거 설정’ 2 에서 구현)를 사용할 수 있다.

✔ 이때 사용자가 정의한 클래스는 Job 설정파일에 빈으로 등록되어 있으므로 어노테이션을 활용한다

public class EgovEventNoticeCallProcessor<T,S> extends EgovStepPostProcessor<T,S> {
 
	//EgovEventNoticeTrigger을 상속받아 재정의한 클래스	
	@Autowired
	EgovEmailEventNoticeTrigger egovEmailEventNoticeTrigger;
 
	public ExitStatus afterStep(StepExecution stepExecution) {
		...
 
		egovEmailEventNoticeTrigger.invoke(stepExecution);
		...
	}
}
  • 트리거 설정
  1. 실제 Email 전송 혹은 SMS 전송을 수행하는 트리거 기능의 클래스를 작성한다. (전자정부 프레임워크 공통컴포넌트 활용 가능)
  2. EgovEventNoticeTrigger 클래스 상속받아 invoke 메소드를 구현한다. 파라메터(JobExecution, StepExecution, Exception 등)에 대한 알림 내용을 작성한 뒤, 위 클래스(‘트리거 설정’ 1 에서 구현) 트리거 메소드의 파라메터로 전달하며 호출한다.

아래 예시에서는 StepExecution 파라메터를 활용하였다.

✔ 이때 사용자가 정의한 클래스는 설정파일에서 빈으로 등록되어 있다.

public class EgovEmailEventNoticeTrigger extends EgovEventNoticeTrigger {
 
	public void invoke(StepExecution stepExecution) {
 
		// StepExecution 을 활용하여 알림내용 작성
		// '트리거 설정' 1에서 구현한 트리거를 통해 메시지 전송 수행
	}
}
<-- 프로세서(리스너) 설정 1  Job 설정파일 (계속) -->
<bean id="EmailEventNoticeTrigger"
 	class="egovframework.brte.sample.example.event.EgovEmailEventNoticeTrigger" />

위 기능을 활용하여 실제 Email을 전송하는 예제를 제공하므로 아래를 참고한다.

사용예시

7.27 - Flow Control

Job 내부에는 여러 Step 들이 존재할 수 있고, 각 Step 사이의 흐름을 관리할 필요가 있다. Step 내의 next 설정과 Desision 설정으로 Job을 수행하다 한 Step의 처리결과에 따라 다른 Step을 선택하여 수행할 수 있고, 특정 Step의 실패가 Job 전체의 실패로 이어지지 않도록 구성할 수 있다.

Flow Control

개요

Job 내부에는 여러 Step 들이 존재할 수 있고, 각 Step 사이의 흐름을 관리할 필요가 있다. Step 내의 next 설정과 Desision 설정으로 Job을 수행하다 한 Step의 처리결과에 따라 다른 Step을 선택하여 수행할 수 있고, 특정 Step의 실패가 Job 전체의 실패로 이어지지 않도록 구성할 수 있다.

설명

흐름 처리(Controlling Step Flow)

Sequential Flow

가장 간단한 시나리오의 Job은 모든 Step을 순서대로 실행 하는 것이다.

sequential-flow

위와 같은 Job의 실행은 Step 엘리먼트의 ’next’ 어트리뷰트를 이용해서 설정할 수 있다.

<job id="job">
    <step id="stepA" parent="s1" next="stepB" />
    <step id="stepB" parent="s2" next="stepC"/>
    <step id="stepC" parent="s3" />
</job>

위 시나리오를 실행 하면 Job 설정의 가장 상단에 위치한 ‘stepA’가 먼저 실행 된다. ‘stepA’가 실행 완료 되면 ‘stepB’, ‘stepC’의 순서로 실행되게 된다. 하지만 만약 ‘stepA’의 실행이 실패하게 된다면 전체 Job의 실행은 실패하게 되며 ‘stepB’는 실행 되지 않는다.

  • Note : 스프링 배치의 XML 설정은 Job 설정의 가장 상단 Step이 최초로 실행되게 되며, 그 후의 Step 실행 순서는 XML 설정 순서와는 관계가 없다.

Conditional Flow

위 Sequential Flow의 실행 결과는 다음 2가지로 나뉘게 된다.

  1. Step이 성공적으로 실행되고, 다음 Step이 실행 된다.
  2. Step의 실행이 실패하며 그로인해 Job의 실행 또한 실패한다.

많은 케이스가 위의 내용에 해당하게 된다. 하지만 Step의 실패가 Job의 실패를 유발하는 것이 아니고 다른 Step을 실행하는 경우를 생각해 보자.

conditional-flow

스프링 배치에서는 다양한 경우의 시나라오를 설정 할 수 있도록 Step 엘리먼트내에 사용할 수 있는 transition 엘리먼트를 제공 해준다. 그중의 한 엘리먼트로 ’next’ 엘리먼트가 있다. ’next’ 엘리먼트는 ’next’ 어트리뷰트와 마찬가지로 Job 실행에서 다음에 실행될 Step에 대해 알려준다. 하지만 ’next’ 어트리뷰트와 다르게 ’next’ 엘리먼트는 사용 횟수에 제한이 없으며, Step의 실패에 대한 default 설정이 없다. 그러므로 transition 엘리먼트를 사용할 때에는 모든 Step의 Behavior 설정을 충분히 해줘야 한다.

✔ 싱글 Step의 경우 ’next’ 어트리뷰트와 transition 엘리먼트를 사용할 수 없다.

다음은 ’next’ 엘리먼트를 사용하는 기본 패턴이다

<job id="job">
    <step id="stepA" parent="s1">
        <next on="*" to="stepB" />
        <next on="FAILED" to="stepC" />
    </step>
    <step id="stepB" parent="s2" next="stepC" />
    <step id="stepC" parent="s3" />
</job>

’next’ 엘리먼트의 ‘on’ 어트리뷰트를 이용해서 Step 실행 결과(ExitStatus)에 따라 다음 Step을 설정 할 수 있다.‘on’ 어트리뷰트에 패턴으로 사용 될 수 있는 특수 문자는 다음 2가지만이 가능하다.

  • ‘*’ : 0 또는 그 이상의 문자.
  • ‘?’ : 하나의 문자.

예를 들면 ‘c*t’는 ‘cat’과 ‘count’와 모두 매칭 된다. 반면에, ‘c?t’는 ‘cat’과 매칭 되지만 ‘count’와 매칭 되지 않는다. Step 설정에 transition 엘리먼트의 사용 횟수에는 제한이 없지만, Step의 실행 결과(ExitStatus)가 Step 설정에 정의 되지 않은 경우에는 스프링 배치 프레임워크는 예외를 던지게 되며 Job의 실행은 실패하게 된다. 스프링 배치 프레임워크는 ‘on’ 어트리뷰트 설정 값을 상세한 설정을 우선해서 먼저 적용한다. 위 설정을 예로 들면 Step의 실행 결과로 ‘FAILED’ ExitStatus를 갖게 되면 ‘StepA’가 아닌 ‘StepC’로 전이 하게 된다.

Batch Status vs. Exit Status

Conditional flow를 사용하는 Job을 설정할때에는 BatchStatus과 ExitStatus의 차이를 아는것이 중요하다. BatchStatus는 Job 또는 Step 의 실행 결과를 스프링 프레임워크에서 기록할 때 사용하는 Property의 집합니다. BatchStatus로 사용 되는 값은 COMPLETED, STARTING, STARTED, STOPPING, STOPPED, FAILED, ABANDONED, UNKNOWN 이다. 대부분의 값들은 단어와 같은 뜻으로 해석하여 이해하면 된다.

다음의 ’next’ 엘리먼트를 사용하는 예제를 보자.

<next on="FAILED" to="stepB" />

위 예제에서 ‘on’ 어트리뷰트가 BatchStatus를 참조하는 것으로 생각되기 쉽지만 실제 참조되는 값은 Step의 ExitStatus이다. ExitStatus의 이름에서 알 수 있듯이 ExitStatus는 Step의 실행 후 상태를 알려주는 값이다. 위 예제를 좀더 쉽게 풀이 하자면 ’exit 코드가 FAILED로 끝나게 되면 StepB로 가라’는 뜻이 된다. 스프링 배치 프레임워크는 디폴트 설정으로 ExitStatus의 exit 코드는 Step의 BatchStatus와 같도록 설정이 되어 있다. 하지만 만약에 exit 코드가 BatchStatus와 달라야 한다면

<step id="step1" parent="s1">
    <end on="FAILED" />
    <next on="COMPLETED WITH SKIPS" to="errorPrint1" />
    <next on="*" to="step2" />
</step>

위 Step의 실행 결과는 다음 3가지가 될 수 있다.

  • Step이 실패하며, Job 또한 실패하게 된다.
  • Step이 성공적으로 완료된다.
  • Step이 성공적으로 완료되며, ‘COMPLETED WITH SKIPS’의 exit 코드로 종료 된다. 이 경우 별도의 Step이 에러를 처리하기 위해 실행 되어져야만 한다.

위 예제의 사용에는 에러가 없지만, 사용자의 의도대로 처리되기 위해서는’COMPLETED WITH SKIPS’ exit 코드를 반환하는 별도의 로직이 필요하다.

public class SkipCheckingListener extends StepExecutionListenerSupport {
 
    public ExitStatus afterStep(StepExecution stepExecution) {
        String exitCode = stepExecution.getExitStatus().getExitCode();
        if (!exitCode.equals(ExitStatus.FAILED.getExitCode()) && 
              stepExecution.getSkipCount() > 0) {
            return new ExitStatus("COMPLETED WITH SKIPS");
        }
        else {
            return null;
        }
    }
 
}

위코드를 설명하면 StepExecutionListener 에서는 먼저 Step이 성공적으로 수행되었는지 체크한다. 그 후 StepExecution의 skip 횟수가 0.1f보다 클경우 ‘COMPLETED WITH SKIPS’의 exit 코드를 갖는 ExitStatus를 반환한다.

Configuring for Stop

BatchStatus와 ExitStatus가 어떻게 결정되는지 알아 보았다. Step의 Status는 코드의 실행 결과에 의해 결정 되는 반면에 Job의 Status는 스프링 배치 설정에 의해 결정 되어 진다. 모든 Job 설정은 최소 하나의 ’transition이 없는 final Step’ 을 갖게 된다. 다음의 예는 Step이 실행 되어 진 후에 Job이 종료 되게 된다.

<step id="stepC" parent="s3"/>

transition이 정의 되지 않은 Step의 경우 Job의 Status는 다음과 같이 결정 되게 된다.

  • Step의 ExitStatus가 FAILED 일 경우 Job의 BatchStatus와 ExitStatus는 모두 FAILED 가 된다.
  • 위 경우와 반대로 Step의 ExitStatus가 COMPLETED 일 경우 Job의 BatchStatus와 ExitStatus는 모두 COMPLETED 가 된다.

단순 Sequential Step의 Job의 경우에는 위 방법만으로도 Job을 설정하기에 충분하지만, 별도의 Job의 중단을 위한 시나리오가 필요한 경우도 존재한다. 이같은 경우를 위해 스프링 배치 프레임워크에서는 Job을 중단시키기 위한 transition 엘리먼트를 제공한다. 이 엘리먼트들은 Job을 특정 BatchStatus와 함께 종료 시킨다.

Stop transition 엘리먼트는 Step의 BatchStatus 또는 ExitStatus에는 영향을 미칠수 없으며, 오로지 Job의 마지막 상태에만 영향을 미칠수 있다. 예를 들어, Job의 모든 Step의 결과가 FAILED이지만 Job의 Status는 COMPLETED가 될 수 있으며 ,그반대 또한 가능하다.

The ‘End’ Element

’end’ 엘리먼트는 Job을 COMPLETED BatchStatus와 함께 종료 시킨다. COMPLETED 상태로 종료된 Job은 재실행이 불가능하다(스프링 배치 프레임워크에서 JobInstanceAlreadyCompleteException이 발생됨). ’end’ 엘리먼트에는 추가적으로 ’exit-code’ 어트리뷰트를 사용하면 커스텀 ExitStatus의 정의가 가능하다. ’exit-code’ 정의가 없는 경우에는 디폴트 코드로 COMPLETED가 적용되게 된다.

다음의 예제는 Step2가 실패하기 되면 Job은 COMPLETED BatchStatus로 종료되며 Step3은 실행 되지않는다. 이경우 Job이 COMPLETED BatchStatus로 종료 되었기 때문에 재시작이 불가능 하다.(Step2가 성공하는 경우에는 Step3이 실행되게 된다)

<step id="step1" parent="s1" next="step2">
 
<step id="step2" parent="s2">
    <end on="FAILED"/>
    <next on="*" to="step3"/>
</step>
 
<step id="step3" parent="s3">
The ‘Fail’ Element

‘fail’ 엘리먼트는 Job을 FAILED BatchStatus와 함께 종료 시킨다. ’end’엘리먼트와 다르게 ‘fail’ 엘리먼트를 사용한 경우에는 Job의 재시작이 가능하다. ‘fail’ 엘리먼트 또한 커스텀 ExitStatus의 정의를 위한 ’exit-code’ 어트리뷰트를 사용 할 수 있다. ’exit-code’ 정의가 없는 경우에는 디폴트 코드로 FAILED가 적용되게 된다.

다음의 예제는 Step2가 실패하기 되면 Job은 FAILED BatchStatus와 EARLY TERMINATION ExitStatus로 종료되며 Step3은 실행 되지 않는다. 이경우 Job의 재시작이 가능하며, 실행은 Step2부터 시작된다.(Step2가 성공하는 경우에는 Step3이 실행되게 된다)

<step id="step1" parent="s1" next="step2">
 
<step id="step2" parent="s2">
    <fail on="FAILED" exit-code="EARLY TERMINATION"/>
    <next on="*" to="step3"/>
</step>
 
<step id="step3" parent="s3">
The ‘Stop’ Element

‘stop’ 엘리먼트는 Job을 STOPPED BatchStatus와 함께 종료 시킨다. Job이 중단 되면 Job이 재시작 되기전에 오퍼레이터는 추가적인 작업을 수행 할 수 있다. ‘stop’ 엘리먼트는 다음 Job의 재실행 지점의 설정을 위해 ‘restart’ 어트리뷰트의 정의가 요구된다.

다음의 예제에서는 Step1이 COMPLETED 상태로 종료가 되면 Job은 정지 되게 되며, 정지된 Job의 재실행 지점은 Step2이

<step id="step1" parent="s1">
    <stop on="COMPLETED" restart="step2"/>
</step>
 
<step id="step2" parent="s2"/>

Programmatic Flow Decisions

경우에 따라서는 다음에 실행될 Step을 결정하기 위해 ExitStatus 보다 더 많은 정보가 필요할 경우가 있다. 이때에 다음 Step을 결정하기 위해 JobExecutionDecider를 이용 할 수 있다.

public class MyDecider implements JobExecutionDecider {
    public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) 
 
{
        if (someCondition) {
            return "FAILED";
        }
        else {
            return "COMPLETED";
        }
    }
}

Job 설정을 위한 ‘decision’ 태그를 다음과 같이 사용한다.

<job id="job">
    <step id="step1" parent="s1" next="decision" />
 
    <decision id="decision" decider="decider">
        <next on="FAILED" to="step2" />
        <next on="COMPLETED" to="step3" />
    </decision>
 
    <step id="step2" parent="s2" next="step3"/>
    <step id="step3" parent="s3" />
</job>
 
<beans:bean id="decider" class="com.MyDecider"/>

Split Flows

앞에서 설명한 모드 시나리에서는 Job내의 Step들의 실행이 순서대로 이루어지고 있다. 스프링 배치프레임워크에서는 이외에도 parallel flow를 지원하기 위한 ‘split’ 엘리먼트를 제공한다.

아래의 코드를 보면 ‘split’ 엘리먼트는 하나 이상의 ‘flow’ 엘리먼트를 갖을 수 있으며, ‘flow’ 엘리먼트는 분리된 flow를 정의한다. ‘split’ 엘리먼트는 앞에서 설명한 ’next’ 어트리뷰트, ’next’ 엘리먼트, ’end’ 엘리먼트, ‘fail’ 엘리먼트, ‘pause’ 엘리먼트들을 포함 할 수 있다

<split id="split1" next="step4">
    <flow>
        <step id="step1" parent="s1" next="step2"/>
        <step id="step2" parent="s2"/>
    </flow>
    <flow>
        <step id="step3" parent="s3"/>
    </flow>
</split>
<step id="step4" parent="s4"/>

Externalizing Flow Definitions and Dependencies Between Jobs

Job의 flow중 일부는 별도의 bean 설정으로 분리 될 수 있으며, 재사용이 가능하다. 이러한 설정 방법에는 3가지가 있으면 그중 첫 번째는 아래와 같이 별도로 설정된 flow를 참조하는 방법이다.

<job id="job">
    <flow id="job1.flow1" parent="flow1" next="step3"/>
    <step id="step3" parent="s3"/>
</job>
 
<flow id="flow1">
    <step id="step1" parent="s1" next="step2"/>
    <step id="step2" parent="s2"/>
</flow>

위와 같이 flow를 설정하게 되면 간단하게 별도의 Step을 추가 할 수 있다. 이런 방법으로 여러 Job에서 같은 flow 템플릿을 참조 할 수 있으며, flow 템플릿의 조합으로 다른 로직의 flow 설정을 할 수 있다. 위 방법은 통합 테스트를 별도의 flow로 분리하는 데에도 유용하게 사용 될 수 있다.

두번째 flow의 분리 방법은 FlowStep을 사용하는 방법이다. FlowStep은 flow의 처리를 <flow/> 엘리먼트로 위임하는 Step 인터페이스의 구현체이다. 아래의 FlowStep 설정 예제를 보자.

<job id="job">
    <step id="job1.flow1" flow="flow1" next="step3"/>
    <step id="step3" parent="s3"/>
</job>
 
<flow id="flow1">
    <step id="step1" parent="s1" next="step2"/>
    <step id="step2" parent="s2"/>
</flow>

실제 로직의 수행은 이전 예제와 동일하나, Job Repository에 저장되는 데이터가 다르게 된다. 이러한 설정 방법은 모니터링 과 리포팅 목적으로 유용하게 사용될 수 있으며 partitioned step의 구조를 더욱 다양하게 해줄 수 있다.

세번째 flow의 분리 방법은 JobStep을 사용하는 방법이다. JobStep은 FlowStep과 유사하지만 분리된 Step의 실행을 위해 별도의 Job Execution을 생성하게 된다.

<job id="jobStepJob" restartable="true">
   <step id="jobStepJob.step1">
      <job ref="job" job-launcher="jobLauncher" 
          job-parameters-extractor="jobParametersExtractor"/>
   </step>
</job>
 
<job id="job" restartable="true">...</job>
 
<bean id="jobParametersExtractor" class="org.spr...DefaultJobParametersExtractor">
   <property name="keys" value="input.file"/>
</bean>

위 예제를 보자. job parameters extractor 설정에서 Job이 실행 될때 Step의 ExecutionContext에서 JobParameters를 얻는 방법에 대해 정의한다. JobStep은 Job과 Step의 모니터링을 할때 좀더 세밀한 옵션을 주고 싶을 때 유용하다. JobStep은 “Job 사이의 dependencies를 어떻게 생성하느냐?“의 질문에 대한 좋은 답이 될 수 있다. 위와 같은 설정은 큰 시스템을 작은 모듈로 분리하고 Job의 flow를 컨트롤하는데 유용하게 사용될 수 있다.

관련예제

Flow Control 활용한 건너뛰기(Skip) 기능 예제

참고자료